33//! This module detects when incoming headers create a fork in the blockchain
44//! rather than extending the current chain tip.
55
6- use super :: { ChainWork , Fork } ;
7- use crate :: storage:: ChainStorage ;
8- use crate :: types:: ChainState ;
9- use dashcore:: { BlockHash , Header as BlockHeader } ;
6+ use super :: Fork ;
7+ use dashcore:: BlockHash ;
108use std:: collections:: HashMap ;
119
1210/// Detects and manages blockchain forks
1311pub struct ForkDetector {
1412 /// Currently known forks indexed by their tip hash
1513 forks : HashMap < BlockHash , Fork > ,
16- /// Maximum number of forks to track
17- max_forks : usize ,
1814}
1915
2016impl ForkDetector {
@@ -24,164 +20,9 @@ impl ForkDetector {
2420 }
2521 Ok ( Self {
2622 forks : HashMap :: new ( ) ,
27- max_forks,
2823 } )
2924 }
3025
31- /// Check if a header creates or extends a fork
32- pub fn check_header < CS : ChainStorage > (
33- & mut self ,
34- header : & BlockHeader ,
35- chain_state : & ChainState ,
36- storage : & CS ,
37- ) -> ForkDetectionResult {
38- let header_hash = header. block_hash ( ) ;
39- let prev_hash = header. prev_blockhash ;
40-
41- // Check if this extends the main chain
42- if let Some ( tip_header) = chain_state. get_tip_header ( ) {
43- tracing:: trace!(
44- "Checking main chain extension - prev_hash: {}, tip_hash: {}" ,
45- prev_hash,
46- tip_header. block_hash( )
47- ) ;
48- if prev_hash == tip_header. block_hash ( ) {
49- return ForkDetectionResult :: ExtendsMainChain ;
50- }
51- } else {
52- // Special case: chain state is empty (shouldn't happen with genesis initialized)
53- // But handle it just in case
54- if chain_state. headers . is_empty ( ) {
55- // Check if this is connecting to genesis in storage
56- if let Ok ( Some ( height) ) = storage. get_header_height ( & prev_hash) {
57- if height == 0 {
58- // This is the first header after genesis
59- return ForkDetectionResult :: ExtendsMainChain ;
60- }
61- }
62- }
63- }
64-
65- // Special case: Check if header connects to genesis which might be at height 0
66- // This handles the case where chain_state has genesis but we're syncing the first real block
67- if chain_state. tip_height ( ) == 0 {
68- if let Some ( genesis_header) = chain_state. header_at_height ( 0 ) {
69- tracing:: debug!(
70- "Checking if header connects to genesis - prev_hash: {}, genesis_hash: {}" ,
71- prev_hash,
72- genesis_header. block_hash( )
73- ) ;
74- if prev_hash == genesis_header. block_hash ( ) {
75- tracing:: info!(
76- "Header extends genesis block - treating as main chain extension"
77- ) ;
78- return ForkDetectionResult :: ExtendsMainChain ;
79- }
80- }
81- }
82-
83- // Check if this extends a known fork
84- // Need to find a fork whose tip matches our prev_hash
85- let matching_fork = self
86- . forks
87- . iter ( )
88- . find ( |( _, fork) | fork. tip_hash == prev_hash)
89- . map ( |( _, fork) | fork. clone ( ) ) ;
90-
91- if let Some ( mut fork) = matching_fork {
92- // Remove the old entry (indexed by old tip)
93- self . forks . remove ( & fork. tip_hash ) ;
94-
95- // Update the fork
96- fork. headers . push ( * header) ;
97- fork. tip_hash = header_hash;
98- fork. tip_height += 1 ;
99- fork. chain_work = fork. chain_work . add_header ( header) ;
100-
101- // Re-insert with new tip hash
102- let result_fork = fork. clone ( ) ;
103- self . forks . insert ( header_hash, fork) ;
104-
105- return ForkDetectionResult :: ExtendsFork ( result_fork) ;
106- }
107-
108- // Check if this connects to the main chain (creates new fork)
109- if let Ok ( Some ( height) ) = storage. get_header_height ( & prev_hash) {
110- // Check if this would create a fork from before our checkpoint
111- if chain_state. synced_from_checkpoint ( ) && height < chain_state. sync_base_height {
112- tracing:: warn!(
113- "Rejecting header that would create fork from height {} (before checkpoint base {}). \
114- This likely indicates headers from genesis were received during checkpoint sync.",
115- height, chain_state. sync_base_height
116- ) ;
117- return ForkDetectionResult :: Orphan ;
118- }
119-
120- // Found connection point - this creates a new fork
121- let fork_height = height;
122- let fork = Fork {
123- fork_point : prev_hash,
124- fork_height,
125- tip_hash : header_hash,
126- tip_height : fork_height + 1 ,
127- headers : vec ! [ * header] ,
128- chain_work : ChainWork :: from_height_and_header ( fork_height, header) ,
129- } ;
130-
131- self . add_fork ( fork. clone ( ) ) ;
132- return ForkDetectionResult :: CreatesNewFork ( fork) ;
133- }
134-
135- // Additional check: see if header connects to any header in chain_state
136- // This helps when storage might be out of sync with chain_state
137- for ( height, state_header) in chain_state. headers . iter ( ) . enumerate ( ) {
138- if prev_hash == state_header. block_hash ( ) {
139- // Calculate the actual blockchain height for this index
140- let actual_height = chain_state. sync_base_height + ( height as u32 ) ;
141-
142- // This connects to a header in chain state but not in storage
143- // Treat it as extending main chain if it's the tip
144- if height == chain_state. headers . len ( ) - 1 {
145- return ForkDetectionResult :: ExtendsMainChain ;
146- } else {
147- // Creates a fork from an earlier point
148- let fork = Fork {
149- fork_point : prev_hash,
150- fork_height : actual_height,
151- tip_hash : header_hash,
152- tip_height : actual_height + 1 ,
153- headers : vec ! [ * header] ,
154- chain_work : ChainWork :: from_height_and_header ( actual_height, header) ,
155- } ;
156-
157- self . add_fork ( fork. clone ( ) ) ;
158- return ForkDetectionResult :: CreatesNewFork ( fork) ;
159- }
160- }
161- }
162-
163- // This header doesn't connect to anything we know
164- ForkDetectionResult :: Orphan
165- }
166-
167- /// Add a new fork to track
168- fn add_fork ( & mut self , fork : Fork ) {
169- self . forks . insert ( fork. tip_hash , fork) ;
170-
171- // Limit the number of forks we track
172- if self . forks . len ( ) > self . max_forks {
173- // Remove the fork with least work
174- if let Some ( weakest) = self . find_weakest_fork ( ) {
175- self . forks . remove ( & weakest) ;
176- }
177- }
178- }
179-
180- /// Find the fork with the least cumulative work
181- fn find_weakest_fork ( & self ) -> Option < BlockHash > {
182- self . forks . iter ( ) . min_by_key ( |( _, fork) | & fork. chain_work ) . map ( |( hash, _) | * hash)
183- }
184-
18526 /// Get all known forks
18627 pub fn get_forks ( & self ) -> Vec < & Fork > {
18728 self . forks . values ( ) . collect ( )
@@ -229,92 +70,6 @@ pub enum ForkDetectionResult {
22970#[ cfg( test) ]
23071mod tests {
23172 use super :: * ;
232- use crate :: storage:: MemoryStorage ;
233- use dashcore:: blockdata:: constants:: genesis_block;
234- use dashcore:: Network ;
235- use dashcore_hashes:: Hash ;
236-
237- fn create_test_header ( prev_hash : BlockHash , nonce : u32 ) -> BlockHeader {
238- let mut header = genesis_block ( Network :: Dash ) . header ;
239- header. prev_blockhash = prev_hash;
240- header. nonce = nonce;
241- header
242- }
243-
244- #[ test]
245- fn test_fork_detection ( ) {
246- let mut detector = ForkDetector :: new ( 10 ) . expect ( "Failed to create fork detector" ) ;
247- let storage = MemoryStorage :: new ( ) ;
248- let mut chain_state = ChainState :: new ( ) ;
249-
250- // Add genesis
251- let genesis = genesis_block ( Network :: Dash ) . header ;
252- storage. store_header ( & genesis, 0 ) . expect ( "Failed to store genesis header" ) ;
253- chain_state. add_header ( genesis) ;
254-
255- // Header that extends main chain
256- let header1 = create_test_header ( genesis. block_hash ( ) , 1 ) ;
257- let result = detector. check_header ( & header1, & chain_state, & storage) ;
258- assert ! ( matches!( result, ForkDetectionResult :: ExtendsMainChain ) ) ;
259-
260- // Add header1 to chain
261- storage. store_header ( & header1, 1 ) . expect ( "Failed to store header1" ) ;
262- chain_state. add_header ( header1) ;
263-
264- // Header that creates a fork from genesis
265- let fork_header = create_test_header ( genesis. block_hash ( ) , 2 ) ;
266- let result = detector. check_header ( & fork_header, & chain_state, & storage) ;
267-
268- match result {
269- ForkDetectionResult :: CreatesNewFork ( fork) => {
270- assert_eq ! ( fork. fork_point, genesis. block_hash( ) ) ;
271- assert_eq ! ( fork. fork_height, 0 ) ;
272- assert_eq ! ( fork. tip_height, 1 ) ;
273- assert_eq ! ( fork. headers. len( ) , 1 ) ;
274- }
275- result => panic ! ( "Expected CreatesNewFork, got {:?}" , result) ,
276- }
277-
278- // Header that extends the fork
279- let fork_header2 = create_test_header ( fork_header. block_hash ( ) , 3 ) ;
280- let result = detector. check_header ( & fork_header2, & chain_state, & storage) ;
281-
282- assert ! ( matches!( result, ForkDetectionResult :: ExtendsFork ( _) ) ) ;
283- assert_eq ! ( detector. get_forks( ) . len( ) , 1 ) ;
284-
285- // Orphan header
286- let orphan = create_test_header (
287- BlockHash :: from_raw_hash ( dashcore_hashes:: hash_x11:: Hash :: all_zeros ( ) ) ,
288- 4 ,
289- ) ;
290- let result = detector. check_header ( & orphan, & chain_state, & storage) ;
291- assert ! ( matches!( result, ForkDetectionResult :: Orphan ) ) ;
292- }
293-
294- #[ test]
295- fn test_fork_limits ( ) {
296- let mut detector = ForkDetector :: new ( 2 ) . expect ( "Failed to create fork detector" ) ;
297- let storage = MemoryStorage :: new ( ) ;
298- let mut chain_state = ChainState :: new ( ) ;
299-
300- // Add genesis
301- let genesis = genesis_block ( Network :: Dash ) . header ;
302- storage. store_header ( & genesis, 0 ) . expect ( "Failed to store genesis header" ) ;
303- chain_state. add_header ( genesis) ;
304-
305- // Add a header to extend the main chain past genesis
306- let header1 = create_test_header ( genesis. block_hash ( ) , 1 ) ;
307- storage. store_header ( & header1, 1 ) . expect ( "Failed to store header1" ) ;
308- chain_state. add_header ( header1) ;
309-
310- // Create 3 forks from genesis, should only keep 2
311- for i in 0 ..3 {
312- let fork_header = create_test_header ( genesis. block_hash ( ) , i + 100 ) ;
313- detector. check_header ( & fork_header, & chain_state, & storage) ;
314- }
315-
316- assert_eq ! ( detector. get_forks( ) . len( ) , 2 ) ;
317- }
31873
31974 #[ test]
32075 fn test_fork_detector_zero_max_forks ( ) {
0 commit comments