12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
+ from typing import TYPE_CHECKING
16
+
15
17
from hathor .feature_activation .feature import Feature
16
18
from hathor .feature_activation .model .feature_description import FeatureDescription
17
19
from hathor .feature_activation .model .feature_state import FeatureState
18
20
from hathor .feature_activation .settings import Settings as FeatureSettings
19
- from hathor .transaction import Block
20
- from hathor .transaction .storage import TransactionStorage
21
+
22
+ if TYPE_CHECKING :
23
+ from hathor .transaction import Block
24
+ from hathor .transaction .storage import TransactionStorage
21
25
22
26
23
27
class FeatureService :
24
28
__slots__ = ('_feature_settings' , '_tx_storage' )
25
29
26
- def __init__ (self , * , feature_settings : FeatureSettings , tx_storage : TransactionStorage ) -> None :
30
+ def __init__ (self , * , feature_settings : FeatureSettings , tx_storage : ' TransactionStorage' ) -> None :
27
31
self ._feature_settings = feature_settings
28
32
self ._tx_storage = tx_storage
29
33
30
- def is_feature_active (self , * , block : Block , feature : Feature ) -> bool :
34
+ def is_feature_active (self , * , block : ' Block' , feature : Feature ) -> bool :
31
35
"""Returns whether a Feature is active at a certain block."""
32
36
state = self .get_state (block = block , feature = feature )
33
37
34
38
return state == FeatureState .ACTIVE
35
39
36
- def get_state (self , * , block : Block , feature : Feature ) -> FeatureState :
40
+ def check_must_signal (self , block : 'Block' ) -> bool :
41
+ """Return whether a block is missing mandatory signaling for any feature currently in the MUST_SIGNAL phase."""
42
+ bit_counts = block .get_feature_activation_bit_counts ()
43
+ height = block .get_height ()
44
+ offset_to_boundary = height % self ._feature_settings .evaluation_interval
45
+ remaining_blocks = self ._feature_settings .evaluation_interval - offset_to_boundary - 1
46
+ descriptions = self .get_bits_description (block = block )
47
+
48
+ must_signal_features = (
49
+ feature for feature , description in descriptions .items ()
50
+ if description .state is FeatureState .MUST_SIGNAL
51
+ )
52
+
53
+ for feature in must_signal_features :
54
+ criteria = self ._feature_settings .features [feature ]
55
+ threshold = criteria .get_threshold (self ._feature_settings )
56
+ count = bit_counts [criteria .bit ]
57
+ missing_signals = threshold - count
58
+
59
+ if missing_signals > remaining_blocks :
60
+ return True
61
+
62
+ return False
63
+
64
+ def get_state (self , * , block : 'Block' , feature : Feature ) -> FeatureState :
37
65
"""Returns the state of a feature at a certain block. Uses block metadata to cache states."""
38
66
39
67
# per definition, the genesis block is in the DEFINED state for all features
@@ -54,6 +82,9 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
54
82
previous_boundary_block = self ._get_ancestor_at_height (block = block , height = previous_boundary_height )
55
83
previous_boundary_state = self .get_state (block = previous_boundary_block , feature = feature )
56
84
85
+ # We cache _and save_ the state of the previous boundary block that we just got.
86
+ previous_boundary_block .set_feature_state (feature = feature , state = previous_boundary_state , save = True )
87
+
57
88
if offset_to_boundary != 0 :
58
89
return previous_boundary_state
59
90
@@ -63,14 +94,16 @@ def get_state(self, *, block: Block, feature: Feature) -> FeatureState:
63
94
previous_state = previous_boundary_state
64
95
)
65
96
66
- block .update_feature_state (feature = feature , state = new_state )
97
+ # We cache the just calculated state of the current block _without saving it_, as it may still be unverified,
98
+ # so we cannot persist its metadata. That's why we cache and save the previous boundary block above.
99
+ block .set_feature_state (feature = feature , state = new_state )
67
100
68
101
return new_state
69
102
70
103
def _calculate_new_state (
71
104
self ,
72
105
* ,
73
- boundary_block : Block ,
106
+ boundary_block : ' Block' ,
74
107
feature : Feature ,
75
108
previous_state : FeatureState
76
109
) -> FeatureState :
@@ -136,7 +169,7 @@ def _calculate_new_state(
136
169
137
170
raise ValueError (f'Unknown previous state: { previous_state } ' )
138
171
139
- def get_bits_description (self , * , block : Block ) -> dict [Feature , FeatureDescription ]:
172
+ def get_bits_description (self , * , block : ' Block' ) -> dict [Feature , FeatureDescription ]:
140
173
"""Returns the criteria definition and feature state for all features at a certain block."""
141
174
return {
142
175
feature : FeatureDescription (
@@ -146,7 +179,7 @@ def get_bits_description(self, *, block: Block) -> dict[Feature, FeatureDescript
146
179
for feature , criteria in self ._feature_settings .features .items ()
147
180
}
148
181
149
- def _get_ancestor_at_height (self , * , block : Block , height : int ) -> Block :
182
+ def _get_ancestor_at_height (self , * , block : ' Block' , height : int ) -> ' Block' :
150
183
"""
151
184
Given a block, returns its ancestor at a specific height.
152
185
Uses the height index if the block is in the best blockchain, or search iteratively otherwise.
@@ -158,13 +191,14 @@ def _get_ancestor_at_height(self, *, block: Block, height: int) -> Block:
158
191
metadata = block .get_metadata ()
159
192
160
193
if not metadata .voided_by and (ancestor := self ._tx_storage .get_transaction_by_height (height )):
194
+ from hathor .transaction import Block
161
195
assert isinstance (ancestor , Block )
162
196
return ancestor
163
197
164
198
return _get_ancestor_iteratively (block = block , ancestor_height = height )
165
199
166
200
167
- def _get_ancestor_iteratively (* , block : Block , ancestor_height : int ) -> Block :
201
+ def _get_ancestor_iteratively (* , block : ' Block' , ancestor_height : int ) -> ' Block' :
168
202
"""Given a block, returns its ancestor at a specific height by iterating over its ancestors. This is slow."""
169
203
# TODO: there are further optimizations to be done here, the latest common block height could be persisted in
170
204
# metadata, so we could still use the height index if the requested height is before that height.
0 commit comments