18
18
import struct
19
19
from collections import OrderedDict
20
20
from enum import Enum
21
- from typing import TYPE_CHECKING , Any , Callable , Generator , Optional , cast
21
+ from typing import TYPE_CHECKING , Any , Callable , Generator , NamedTuple , Optional , cast
22
22
23
23
from structlog import get_logger
24
24
from twisted .internet .defer import Deferred , inlineCallbacks
44
44
MAX_GET_TRANSACTIONS_BFS_LEN : int = 8
45
45
46
46
47
+ class BlockInfo (NamedTuple ):
48
+ height : int
49
+ id : VertexId
50
+
51
+ def __repr__ (self ):
52
+ return f'BlockInfo({ self .height } , { self .id .hex ()} )'
53
+
54
+
47
55
class PeerState (Enum ):
48
56
ERROR = 'error'
49
57
UNKNOWN = 'unknown'
@@ -92,16 +100,16 @@ def __init__(self, protocol: 'HathorProtocol', reactor: Optional[Reactor] = None
92
100
self .receiving_stream = False
93
101
94
102
# highest block where we are synced
95
- self .synced_height = 0
103
+ self .synced_block : Optional [ BlockInfo ] = None
96
104
97
105
# highest block peer has
98
- self .peer_height = 0
106
+ self .peer_best_block : Optional [ BlockInfo ] = None
99
107
100
108
# Latest deferred waiting for a reply.
101
109
self ._deferred_txs : dict [VertexId , Deferred [BaseTransaction ]] = {}
102
110
self ._deferred_tips : Optional [Deferred [list [bytes ]]] = None
103
- self ._deferred_best_block : Optional [Deferred [dict [ str , Any ] ]] = None
104
- self ._deferred_peer_block_hashes : Optional [Deferred [list [tuple [ int , bytes ] ]]] = None
111
+ self ._deferred_best_block : Optional [Deferred [BlockInfo ]] = None
112
+ self ._deferred_peer_block_hashes : Optional [Deferred [list [BlockInfo ]]] = None
105
113
106
114
# When syncing blocks we start streaming with all peers
107
115
# so the moment I get some repeated blocks, I stop the download
@@ -151,8 +159,8 @@ def get_status(self) -> dict[str, Any]:
151
159
"""
152
160
res = {
153
161
'is_enabled' : self .is_sync_enabled (),
154
- 'peer_height ' : self .peer_height ,
155
- 'synced_height ' : self .synced_height ,
162
+ 'peer_best_block ' : self .peer_best_block ,
163
+ 'synced_block ' : self .synced_block ,
156
164
'synced' : self ._synced ,
157
165
'state' : self .state .value ,
158
166
}
@@ -332,37 +340,43 @@ def run_sync_transactions(self) -> None:
332
340
end_block_height = block_height )
333
341
self .send_get_transactions_bfs (needed_txs , block .hash )
334
342
343
+ def get_my_best_block (self ) -> BlockInfo :
344
+ """Return my best block info."""
345
+ bestblock = self .tx_storage .get_best_block ()
346
+ assert bestblock .hash is not None
347
+ meta = bestblock .get_metadata ()
348
+ assert not meta .voided_by
349
+ assert meta .validation .is_fully_connected ()
350
+ return BlockInfo (height = bestblock .get_height (), id = bestblock .hash )
351
+
335
352
@inlineCallbacks
336
353
def run_sync_blocks (self ) -> Generator [Any , Any , None ]:
337
354
""" Async step of the block syncing phase.
338
355
"""
339
356
assert self .tx_storage .indexes is not None
340
357
self .state = PeerState .SYNCING_BLOCKS
341
358
342
- # Find my height
343
- bestblock = self .tx_storage .get_best_block ()
344
- assert bestblock .hash is not None
345
- meta = bestblock .get_metadata ()
346
- my_height = meta .height
359
+ # Get my best block.
360
+ my_best_block = self .get_my_best_block ()
347
361
348
- self .log .debug ('run sync blocks' , my_height = my_height )
362
+ # Find peer's best block
363
+ self .peer_best_block = yield self .get_peer_best_block ()
364
+ assert self .peer_best_block is not None
349
365
350
- # Find best block
351
- data = yield self .get_peer_best_block ()
352
- peer_best_block = data ['block' ]
353
- peer_best_height = data ['height' ]
354
- self .peer_height = peer_best_height
366
+ self .log .debug ('run_sync_blocks' , my_best_block = my_best_block , peer_best_block = self .peer_best_block )
355
367
356
368
# find best common block
357
- yield self .find_best_common_block (peer_best_height , peer_best_block )
358
- self .log .debug ('run_sync_blocks' , peer_height = self .peer_height , synced_height = self .synced_height )
369
+ self .synced_block = yield self .find_best_common_block (my_best_block , self .peer_best_block )
370
+ assert self .synced_block is not None
371
+ self .log .debug ('run_sync_blocks' , peer_best_block = self .peer_best_block , synced_block = self .synced_block )
359
372
360
- if self .synced_height < self .peer_height :
373
+ if self .synced_block . height < self .peer_best_block . height :
361
374
# sync from common block
362
- peer_block_at_height = yield self .get_peer_block_hashes ([self .synced_height ])
363
- if peer_block_at_height :
364
- self .run_block_sync (peer_block_at_height [0 ][1 ], self .synced_height , peer_best_block , peer_best_height )
365
- elif my_height == self .synced_height == self .peer_height :
375
+ self .run_block_sync (self .synced_block .id ,
376
+ self .synced_block .height ,
377
+ self .peer_best_block .id ,
378
+ self .peer_best_block .height )
379
+ elif my_best_block .height == self .synced_block .height == self .peer_best_block .height :
366
380
# we're synced and on the same height, get their mempool
367
381
self .state = PeerState .SYNCING_MEMPOOL
368
382
self .mempool_manager .run ()
@@ -494,68 +508,67 @@ def partial_vertex_exists(self, vertex_id: VertexId) -> bool:
494
508
return self .tx_storage .transaction_exists (vertex_id )
495
509
496
510
@inlineCallbacks
497
- def find_best_common_block (self , peer_best_height : int , peer_best_block : bytes ) -> Generator [Any , Any , None ]:
511
+ def find_best_common_block (self ,
512
+ my_best_block : BlockInfo ,
513
+ peer_best_block : BlockInfo ) -> Generator [Any , Any , BlockInfo ]:
498
514
""" Search for the highest block/height where we're synced.
499
515
"""
500
- assert self .tx_storage .indexes is not None
501
- my_best_height = self .tx_storage .get_height_best_block ()
516
+ self .log .debug ('find_best_common_block' , peer_best_block = peer_best_block , my_best_block = my_best_block )
502
517
503
- self .log .debug ('find common chain' , peer_height = peer_best_height , my_height = my_best_height )
504
-
505
- if peer_best_height <= my_best_height :
506
- my_block = self .tx_storage .indexes .height .get (peer_best_height )
507
- if my_block == peer_best_block :
518
+ if peer_best_block .height <= my_best_block .height :
519
+ if peer_best_block .id == my_best_block .id :
508
520
# we have all the peer's blocks
509
- if peer_best_height == my_best_height :
521
+ if peer_best_block . height == my_best_block . height :
510
522
# We are in sync, ask for relay so the remote sends transactions in real time
511
523
self .update_synced (True )
512
524
self .send_relay ()
513
525
else :
514
526
self .update_synced (False )
515
527
516
- self .log .debug ('synced to the latest peer block' , height = peer_best_height )
517
- self .synced_height = peer_best_height
518
- return
528
+ self .log .debug ('synced to the latest peer block' , peer_best_block = peer_best_block )
529
+ return peer_best_block
519
530
else :
520
- # TODO peer is on a different best chain
521
- self .log .warn ('peer on different chain' , peer_height = peer_best_height ,
522
- peer_block = peer_best_block . hex (), my_block = ( my_block . hex () if my_block is not None else
523
- None ) )
531
+ # peer is on a different best chain
532
+ self .log .warn ('peer on different chain' ,
533
+ peer_best_block = peer_best_block ,
534
+ my_best_block = my_best_block )
524
535
525
536
self .update_synced (False )
526
- not_synced = min (peer_best_height , my_best_height )
527
- synced = self .synced_height
528
-
529
- while not_synced - synced > 1 :
530
- self .log .debug ('find_best_common_block synced not_synced' , synced = synced , not_synced = not_synced )
531
- step = math .ceil ((not_synced - synced )/ 10 )
532
- heights = []
533
- height = synced
534
- while height < not_synced :
535
- heights .append (height )
536
- height += step
537
- heights .append (not_synced )
537
+
538
+ # Run an n-ary search in the interval [lo, hi).
539
+ # `lo` is always a height where we are synced.
540
+ # `hi` is always a height where sync state is unknown.
541
+ hi = min (peer_best_block .height , my_best_block .height )
542
+ lo = self .synced_block .height if self .synced_block else 0
543
+
544
+ last_block_hash = self ._settings .GENESIS_BLOCK_HASH
545
+
546
+ while hi - lo > 1 :
547
+ self .log .info ('find_best_common_block n-ary search query' , lo = lo , hi = hi )
548
+ step = math .ceil ((hi - lo ) / 10 )
549
+ heights = list (range (lo , hi , step ))
550
+ heights .append (hi )
551
+
538
552
block_height_list = yield self .get_peer_block_hashes (heights )
539
553
block_height_list .reverse ()
540
554
for height , block_hash in block_height_list :
541
555
try :
542
556
# We must check only fully validated transactions.
543
557
blk = self .tx_storage .get_transaction (block_hash )
558
+ except TransactionDoesNotExist :
559
+ hi = height
560
+ else :
544
561
assert blk .get_metadata ().validation .is_fully_connected ()
545
562
assert isinstance (blk , Block )
546
- if height != blk .get_height ():
547
- # WTF?! It should never happen.
548
- self .state = PeerState .ERROR
549
- return
550
- synced = height
563
+ assert height == blk .get_height ()
564
+ lo = height
565
+ last_block_hash = block_hash
551
566
break
552
- except TransactionDoesNotExist :
553
- not_synced = height
554
567
555
- self .log .debug ('find_best_common_block finished synced not_synced ' , synced = synced , not_synced = not_synced )
556
- self . synced_height = synced
568
+ self .log .debug ('find_best_common_block n-ary search finished ' , lo = lo , hi = hi )
569
+ return BlockInfo ( height = lo , id = last_block_hash )
557
570
558
- def get_peer_block_hashes (self , heights : list [int ]) -> Deferred [list [tuple [ int , bytes ] ]]:
571
+ def get_peer_block_hashes (self , heights : list [int ]) -> Deferred [list [BlockInfo ]]:
559
572
""" Returns the peer's block hashes in the given heights.
560
573
"""
561
574
if self ._deferred_peer_block_hashes is not None :
@@ -793,7 +806,7 @@ def handle_stop_block_streaming(self, payload: str) -> None:
793
806
self .blockchain_streaming .stop ()
794
807
self .blockchain_streaming = None
795
808
796
- def get_peer_best_block (self ) -> Deferred [dict [ str , Any ] ]:
809
+ def get_peer_best_block (self ) -> Deferred [BlockInfo ]:
797
810
""" Async call to get the remote peer's best block.
798
811
"""
799
812
if self ._deferred_best_block is not None :
@@ -813,21 +826,23 @@ def handle_get_best_block(self, payload: str) -> None:
813
826
"""
814
827
best_block = self .tx_storage .get_best_block ()
815
828
meta = best_block .get_metadata ()
829
+ assert meta .validation .is_fully_connected ()
830
+ assert not meta .voided_by
816
831
data = {'block' : best_block .hash_hex , 'height' : meta .height }
817
832
self .send_message (ProtocolMessages .BEST_BLOCK , json .dumps (data ))
818
833
819
834
def handle_best_block (self , payload : str ) -> None :
820
835
""" Handle a BEST-BLOCK message.
821
836
"""
822
837
data = json .loads (payload )
823
- assert self . protocol . connections is not None
824
- self . log . debug ( 'got best block' , ** data )
825
- data [ 'block' ] = bytes . fromhex ( data [ 'block' ] )
838
+ _id = bytes . fromhex ( data [ 'block' ])
839
+ height = data [ 'height' ]
840
+ best_block = BlockInfo ( height = height , id = _id )
826
841
827
842
deferred = self ._deferred_best_block
828
843
self ._deferred_best_block = None
829
844
if deferred :
830
- deferred .callback (data )
845
+ deferred .callback (best_block )
831
846
832
847
def _setup_tx_streaming (self ):
833
848
""" Common setup before starting an outgoing transaction stream.
0 commit comments