@@ -34,6 +34,9 @@ class MemoryDepsIndex(DepsIndex):
34
34
_txs_with_deps_ready : set [bytes ]
35
35
36
36
# Next to be downloaded
37
+ # - Key: hash of the tx to be downloaded
38
+ # - Value[0]: height
39
+ # - Value[1]: hash of the tx waiting for the download
37
40
_needed_txs_index : dict [bytes , tuple [int , bytes ]]
38
41
39
42
def __init__ (self ):
@@ -49,10 +52,11 @@ def force_clear(self) -> None:
49
52
self ._needed_txs_index = {}
50
53
51
54
def add_tx (self , tx : BaseTransaction , partial : bool = True ) -> None :
52
- assert tx .hash is not None
53
- assert tx .storage is not None
54
55
validation = tx .get_metadata ().validation
55
56
if validation .is_fully_connected ():
57
+ # discover if new txs are ready because of this tx
58
+ self ._update_new_deps_ready (tx )
59
+ # finally remove from rev deps
56
60
self ._del_from_deps_index (tx )
57
61
elif not partial :
58
62
raise ValueError ('partial=False will only accept fully connected transactions' )
@@ -63,6 +67,19 @@ def add_tx(self, tx: BaseTransaction, partial: bool = True) -> None:
63
67
def del_tx (self , tx : BaseTransaction ) -> None :
64
68
self ._del_from_deps_index (tx )
65
69
70
+ def _update_new_deps_ready (self , tx : BaseTransaction ) -> None :
71
+ """Go over the reverse dependencies of tx and check if any of them are now ready to be validated.
72
+
73
+ This is also idempotent.
74
+ """
75
+ assert tx .hash is not None
76
+ assert tx .storage is not None
77
+ for candidate_hash in self ._rev_dep_index .get (tx .hash , []):
78
+ with tx .storage .allow_partially_validated_context ():
79
+ candidate_tx = tx .storage .get_transaction (candidate_hash )
80
+ if candidate_tx .is_ready_for_validation ():
81
+ self ._txs_with_deps_ready .add (candidate_hash )
82
+
66
83
def _add_deps (self , tx : BaseTransaction ) -> None :
67
84
"""This method is idempotent, because self.update needs it to be indempotent."""
68
85
assert tx .hash is not None
@@ -94,7 +111,9 @@ def next_ready_for_validation(self, tx_storage: 'TransactionStorage', *, dry_run
94
111
else :
95
112
cur_ready , self ._txs_with_deps_ready = self ._txs_with_deps_ready , set ()
96
113
while cur_ready :
97
- yield from sorted (cur_ready , key = lambda tx_hash : tx_storage .get_transaction (tx_hash ).timestamp )
114
+ with tx_storage .allow_partially_validated_context ():
115
+ sorted_cur_ready = sorted (cur_ready , key = lambda tx_hash : tx_storage .get_transaction (tx_hash ).timestamp )
116
+ yield from sorted_cur_ready
98
117
if dry_run :
99
118
cur_ready = self ._txs_with_deps_ready - cur_ready
100
119
else :
@@ -113,7 +132,8 @@ def _get_rev_deps(self, tx: bytes) -> frozenset[bytes]:
113
132
def known_children (self , tx : BaseTransaction ) -> list [bytes ]:
114
133
assert tx .hash is not None
115
134
assert tx .storage is not None
116
- it_rev_deps = map (tx .storage .get_transaction , self ._get_rev_deps (tx .hash ))
135
+ with tx .storage .allow_partially_validated_context ():
136
+ it_rev_deps = map (tx .storage .get_transaction , self ._get_rev_deps (tx .hash ))
117
137
return [not_none (rev .hash ) for rev in it_rev_deps if tx .hash in rev .parents ]
118
138
119
139
# needed-txs-index methods:
@@ -127,18 +147,13 @@ def is_tx_needed(self, tx: bytes) -> bool:
127
147
def remove_from_needed_index (self , tx : bytes ) -> None :
128
148
self ._needed_txs_index .pop (tx , None )
129
149
130
- def get_next_needed_tx (self ) -> bytes :
131
- # This strategy maximizes the chance to download multiple txs on the same stream
132
- # find the tx with highest "height"
133
- # XXX: we could cache this onto `needed_txs` so we don't have to fetch txs every time
134
- # TODO: improve this by using some sorted data structure to make this better than O(n)
135
- height , start_hash , tx = max ((h , s , t ) for t , (h , s ) in self ._needed_txs_index .items ())
136
- self .log .debug ('next needed tx start' , needed = len (self ._needed_txs_index ), start = start_hash .hex (),
137
- height = height , needed_tx = tx .hex ())
138
- return start_hash
150
+ def iter_next_needed_txs (self ) -> Iterator [bytes ]:
151
+ for tx_hash , _ in self ._needed_txs_index .items ():
152
+ yield tx_hash
139
153
140
154
def _add_needed (self , tx : BaseTransaction ) -> None :
141
155
"""This method is idempotent, because self.update needs it to be indempotent."""
156
+ assert tx .hash is not None
142
157
assert tx .storage is not None
143
158
tx_storage = tx .storage
144
159
@@ -147,9 +162,14 @@ def _add_needed(self, tx: BaseTransaction) -> None:
147
162
# get_all_dependencies is needed to ensure that we get the inputs that aren't reachable through parents alone,
148
163
# this can happen for inputs that have not been confirmed as of the block the confirms the block or transaction
149
164
# that we're adding the dependencies of
150
- for tx_hash in tx .get_all_dependencies ():
165
+ for dep_hash in tx .get_all_dependencies ():
151
166
# It may happen that we have one of the dependencies already, so just add the ones we don't
152
167
# have. We should add at least one dependency, otherwise this tx should be full validated
153
- if not tx_storage .transaction_exists (tx_hash ):
154
- self .log .debug ('tx parent is needed' , tx = tx_hash .hex ())
155
- self ._needed_txs_index [tx_hash ] = (height , not_none (tx .hash ))
168
+ with tx_storage .allow_partially_validated_context ():
169
+ tx_exists = tx_storage .transaction_exists (dep_hash )
170
+ if not tx_exists :
171
+ self .log .debug ('tx parent is needed' , tx = dep_hash .hex ())
172
+ self ._needed_txs_index [dep_hash ] = (height , not_none (tx .hash ))
173
+
174
+ # also, remove the given transaction from needed, because we already have it
175
+ self ._needed_txs_index .pop (tx .hash , None )
0 commit comments