@@ -41,6 +41,12 @@ pub struct DBStore {
4141 /// Whether we are in Initial Block Download mode.
4242 /// During initial block download we skip reorg data writes, since reorgs are
4343 /// extremely unlikely for old blocks and the data is only needed near tip.
44+ ///
45+ /// Every process start begins in IBD mode, so the blocks task turns it off as soon as
46+ /// the block being indexed is within `reorg_data_keep_heights` of the node tip, not
47+ /// only when the tip is reached: a block indexed without reorg data can never be
48+ /// rolled back, and the blocks indexed while catching up after a restart are exactly
49+ /// the ones a reorg is most likely to hit.
4450 ibd : AtomicBool ,
4551
4652 /// Number of recent block heights to keep reorg data for. Older reorg data is automatically deleted.
@@ -687,7 +693,10 @@ impl Store for DBStore {
687693
688694 // Store reorg data for potential blockchain reorganization correction
689695 // Skip during IBD (Initial Block Download) as reorgs are extremely unlikely for old blocks
690- // and this saves significant write overhead during initial sync
696+ // and this saves significant write overhead during initial sync.
697+ // A block written here without reorg data cannot be rolled back later, so the
698+ // caller must call `ibd_finished` before indexing any block that is within
699+ // `reorg_data_keep_heights` of the node tip (see `threads::blocks`).
691700 if !self . ibd . load ( Ordering :: Relaxed ) {
692701 // Create ReorgData and persist it to the database
693702 let reorg_data = ReorgData {
@@ -724,8 +733,9 @@ impl Store for DBStore {
724733 }
725734
726735 fn ibd_finished ( & self ) {
727- log:: info!( "Initial block download finished, enabling reorg data writes" ) ;
728- self . ibd . store ( false , Ordering :: Relaxed ) ;
736+ if self . ibd . swap ( false , Ordering :: Relaxed ) {
737+ log:: info!( "Initial block download finished, enabling reorg data writes" ) ;
738+ }
729739 }
730740}
731741
@@ -814,19 +824,22 @@ fn concat_merge(
814824mod test {
815825 use elements:: { hashes:: Hash , BlockHash , Txid } ;
816826 use rocksdb:: DB ;
817- use std:: { collections:: BTreeMap , sync:: atomic:: AtomicBool } ;
827+ use std:: {
828+ collections:: BTreeMap ,
829+ sync:: atomic:: { AtomicBool , Ordering } ,
830+ } ;
818831
819832 use crate :: store:: {
820833 db:: {
821834 estimate_history_size, get_or_init_salt, serialize_outpoint, vec_tx_seen_from_be_bytes,
822835 vec_tx_seen_to_be_bytes, TxSeen ,
823836 } ,
824- Store ,
837+ BlockMeta , Store ,
825838 } ;
826839 use crate :: OutPoint ;
827840 use crate :: V ;
828841
829- use super :: DBStore ;
842+ use super :: { DBStore , REORG_CF } ;
830843
831844 #[ test]
832845 fn test_db_hash_compatibility ( ) {
@@ -911,6 +924,58 @@ mod test {
911924 // assert_eq!(r, vec![BlockHash::all_zeros(); 3]);
912925 }
913926
927+ #[ test]
928+ fn test_reorg_data_written_only_after_ibd_finished ( ) {
929+ let tempdir = tempfile:: TempDir :: new ( ) . unwrap ( ) ;
930+ let db = DBStore :: open ( tempdir. path ( ) , 64 , false , 6 ) . unwrap ( ) ;
931+ let reorg_cf = db. db . cf_handle ( REORG_CF ) . unwrap ( ) ;
932+ let txid = crate :: be:: Txid :: all_zeros ( ) ;
933+
934+ let block = |height : u32 | BlockMeta :: new ( height, BlockHash :: all_zeros ( ) , height) ;
935+ let created = |vout : u32 | {
936+ let mut m = BTreeMap :: new ( ) ;
937+ m. insert ( OutPoint :: new ( txid, vout) , 1u64 ) ;
938+ m
939+ } ;
940+
941+ // While in IBD the block is indexed but no reorg data is persisted...
942+ db. update ( & block ( 1 ) , vec ! [ ] , BTreeMap :: new ( ) , created ( 1 ) )
943+ . unwrap ( ) ;
944+ assert ! ( db
945+ . db
946+ . get_cf( & reorg_cf, 1u32 . to_be_bytes( ) )
947+ . unwrap( )
948+ . is_none( ) ) ;
949+ // ...so that block can never be rolled back.
950+ let err = db. _reorg ( 1 ) . unwrap_err ( ) . to_string ( ) ;
951+ assert ! ( err. contains( "No reorg data found for height 1" ) , "{err}" ) ;
952+
953+ // Once IBD is finished every indexed block gets reorg data.
954+ db. ibd_finished ( ) ;
955+ db. update ( & block ( 2 ) , vec ! [ ] , BTreeMap :: new ( ) , created ( 2 ) )
956+ . unwrap ( ) ;
957+ assert ! ( db
958+ . db
959+ . get_cf( & reorg_cf, 2u32 . to_be_bytes( ) )
960+ . unwrap( )
961+ . is_some( ) ) ;
962+ assert_eq ! (
963+ db. get_utxos( & [ OutPoint :: new( txid, 2 ) ] ) . unwrap( ) ,
964+ vec![ Some ( 1 ) ]
965+ ) ;
966+
967+ // Calling it again is a no-op, and the block can be rolled back.
968+ db. ibd_finished ( ) ;
969+ assert ! ( !db. ibd. load( Ordering :: Relaxed ) ) ;
970+ db. reorg ( 2 ) ;
971+ assert ! ( db
972+ . db
973+ . get_cf( & reorg_cf, 2u32 . to_be_bytes( ) )
974+ . unwrap( )
975+ . is_none( ) ) ;
976+ assert_eq ! ( db. get_utxos( & [ OutPoint :: new( txid, 2 ) ] ) . unwrap( ) , vec![ None ] ) ;
977+ }
978+
914979 #[ test]
915980 fn test_static_txseen_round_trip ( ) {
916981 let txseen = TxSeen :: new ( crate :: be:: Txid :: all_zeros ( ) , 0 , V :: Undefined ) ;
0 commit comments