@@ -1231,33 +1231,165 @@ func TestGetNextBlockToProcess_ReorgDetected(t *testing.T) {
12311231 // Create mock blockchain client
12321232 mockClient := & blockchain.Mock {}
12331233
1234- // Create mock block for the last persisted block (height 100)
1235- lastPersistedHash , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000064" )
1234+ // Create common ancestor header first so we can use its computed hash
1235+ dummyHash , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000001" )
1236+ nBits , _ := model .NewNBitFromString ("1d00ffff" )
1237+ commonAncestorHeader := & model.BlockHeader {
1238+ Version : 1 ,
1239+ HashPrevBlock : dummyHash ,
1240+ HashMerkleRoot : dummyHash ,
1241+ Timestamp : 1234567890 ,
1242+ Bits : * nBits ,
1243+ Nonce : 1 ,
1244+ }
1245+
1246+ // Use the computed hash as the last persisted hash
1247+ lastPersistedHash := commonAncestorHeader .Hash ()
12361248 lastPersistedBlock := & model.Block {
12371249 Height : 100 ,
12381250 ID : 100 ,
12391251 }
12401252
1241- // Mock the GetBlock call for reorg detection
1253+ // Mock the GetBlock call for reorg detection - using computed hash
12421254 mockClient .On ("GetBlock" , ctx , lastPersistedHash ).Return (
12431255 lastPersistedBlock , nil )
12441256 // Mock the CheckBlockIsInCurrentChain call - returning false to simulate reorg
12451257 mockClient .On ("CheckBlockIsInCurrentChain" , ctx , []uint32 {uint32 (100 )}).Return (
12461258 false , nil ) // false indicates block is NOT on current chain (reorg detected)
1247- // GetBestBlockHeader should NOT be called because we return early
1259+
1260+ // Mock recovery flow - new approach walks backward trying each height
1261+ // It will try to get the block at height 100 from current chain
1262+ // and that will match our state file, so recovery succeeds immediately
1263+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (100 )).Return (
1264+ & model.Block {Height : 100 , Header : commonAncestorHeader }, nil )
12481265
12491266 server := New (ctx , logger , tSettings , nil , nil , nil , mockClient )
12501267
1251- // Set initial persisted height with the orphaned hash
1268+ // Set initial persisted height with the computed hash
1269+ // This ensures the hash in the state file matches what we'll try to rollback to
12521270 err := server .state .AddBlock (100 , lastPersistedHash .String ())
12531271 require .NoError (t , err )
12541272
12551273 // Call getNextBlockToProcess
12561274 block , err := server .getNextBlockToProcess (ctx )
12571275
1258- // Verify that we return nil block and nil error (triggering recovery)
1276+ // Verify that we return nil block and nil error after recovery
1277+ require .NoError (t , err )
1278+ assert .Nil (t , block , "Should return nil block after recovery to trigger retry" )
1279+
1280+ // Verify state wasn't changed since common ancestor is same as last persisted
1281+ height , hash , err := server .state .GetLastPersistedBlock ()
1282+ require .NoError (t , err )
1283+ require .Equal (t , uint32 (100 ), height )
1284+ require .Equal (t , commonAncestorHeader .Hash ().String (), hash .String ())
1285+
1286+ // Verify mock expectations
1287+ mockClient .AssertExpectations (t )
1288+ }
1289+
1290+ // TestGetNextBlockToProcess_ReorgRecovery tests the full reorg recovery flow using block locators
1291+ func TestGetNextBlockToProcess_ReorgRecovery (t * testing.T ) {
1292+ ctx := context .Background ()
1293+ logger := ulogger.TestLogger {}
1294+ tSettings := test .CreateBaseTestSettings (t )
1295+
1296+ // Create temp directory for state file
1297+ tempDir := t .TempDir ()
1298+ tSettings .Block .StateFile = tempDir + "/blocks.dat"
1299+ tSettings .Block .BlockPersisterPersistAge = 2
1300+
1301+ // Create mock blockchain client
1302+ mockClient := & blockchain.Mock {}
1303+
1304+ // Simulate state with blocks up to height 105 (on old chain)
1305+ // Common ancestor is at height 100
1306+ // Reorg occurred at height 101
1307+
1308+ // Create common ancestor header first so we can use its computed hash everywhere
1309+ dummyHash , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000001" )
1310+ nBits , _ := model .NewNBitFromString ("1d00ffff" )
1311+ commonAncestorHeader := & model.BlockHeader {
1312+ Version : 1 ,
1313+ HashPrevBlock : dummyHash ,
1314+ HashMerkleRoot : dummyHash ,
1315+ Timestamp : 1234567890 ,
1316+ Bits : * nBits ,
1317+ Nonce : 1 ,
1318+ }
1319+ commonAncestorHash := commonAncestorHeader .Hash ()
1320+
1321+ // Old chain hashes (blocks after the fork)
1322+ oldChainHash101 , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000101" )
1323+ oldChainHash102 , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000102" )
1324+ oldChainHash103 , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000103" )
1325+ oldChainHash104 , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000104" )
1326+ oldChainHash105 , _ := chainhash .NewHashFromStr ("0000000000000000000000000000000000000000000000000000000000000105" )
1327+
1328+ server := New (ctx , logger , tSettings , nil , nil , nil , mockClient )
1329+
1330+ // Set up state with blocks from old chain using the computed common ancestor hash
1331+ require .NoError (t , server .state .AddBlock (100 , commonAncestorHash .String ()))
1332+ require .NoError (t , server .state .AddBlock (101 , oldChainHash101 .String ()))
1333+ require .NoError (t , server .state .AddBlock (102 , oldChainHash102 .String ()))
1334+ require .NoError (t , server .state .AddBlock (103 , oldChainHash103 .String ()))
1335+ require .NoError (t , server .state .AddBlock (104 , oldChainHash104 .String ()))
1336+ require .NoError (t , server .state .AddBlock (105 , oldChainHash105 .String ()))
1337+
1338+ // Verify initial state
1339+ height , hash , err := server .state .GetLastPersistedBlock ()
1340+ require .NoError (t , err )
1341+ require .Equal (t , uint32 (105 ), height )
1342+ require .Equal (t , oldChainHash105 .String (), hash .String ())
1343+
1344+ // Mock the reorg detection sequence
1345+ lastPersistedBlock := & model.Block {
1346+ Height : 105 ,
1347+ ID : 105 ,
1348+ }
1349+
1350+ // 1. GetBlock call for last persisted block
1351+ mockClient .On ("GetBlock" , ctx , oldChainHash105 ).Return (lastPersistedBlock , nil )
1352+
1353+ // 2. CheckBlockIsInCurrentChain returns false (reorg detected)
1354+ mockClient .On ("CheckBlockIsInCurrentChain" , ctx , []uint32 {uint32 (105 )}).Return (false , nil )
1355+
1356+ // 3. Mock recovery flow - new approach walks backward from height 105
1357+ // trying to find a block from current chain that exists in our state file
1358+ // Heights 105, 104, 103, 102, 101 won't match (different hashes)
1359+ // Height 100 will match (common ancestor)
1360+ newChainHash105 , _ := chainhash .NewHashFromStr ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" )
1361+ newChainHash104 , _ := chainhash .NewHashFromStr ("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" )
1362+ newChainHash103 , _ := chainhash .NewHashFromStr ("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" )
1363+ newChainHash102 , _ := chainhash .NewHashFromStr ("dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd" )
1364+ newChainHash101 , _ := chainhash .NewHashFromStr ("eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" )
1365+
1366+ // Mock GetBlockByHeight for each height during backward search
1367+ // Create proper block headers for each height on the new chain
1368+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (105 )).Return (
1369+ & model.Block {Height : 105 , Header : & model.BlockHeader {Version : 1 , HashPrevBlock : oldChainHash104 , HashMerkleRoot : newChainHash105 , Timestamp : 1234567890 , Bits : * nBits , Nonce : 1 }}, nil )
1370+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (104 )).Return (
1371+ & model.Block {Height : 104 , Header : & model.BlockHeader {Version : 1 , HashPrevBlock : oldChainHash103 , HashMerkleRoot : newChainHash104 , Timestamp : 1234567890 , Bits : * nBits , Nonce : 1 }}, nil )
1372+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (103 )).Return (
1373+ & model.Block {Height : 103 , Header : & model.BlockHeader {Version : 1 , HashPrevBlock : oldChainHash102 , HashMerkleRoot : newChainHash103 , Timestamp : 1234567890 , Bits : * nBits , Nonce : 1 }}, nil )
1374+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (102 )).Return (
1375+ & model.Block {Height : 102 , Header : & model.BlockHeader {Version : 1 , HashPrevBlock : oldChainHash101 , HashMerkleRoot : newChainHash102 , Timestamp : 1234567890 , Bits : * nBits , Nonce : 1 }}, nil )
1376+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (101 )).Return (
1377+ & model.Block {Height : 101 , Header : & model.BlockHeader {Version : 1 , HashPrevBlock : commonAncestorHash , HashMerkleRoot : newChainHash101 , Timestamp : 1234567890 , Bits : * nBits , Nonce : 1 }}, nil )
1378+ mockClient .On ("GetBlockByHeight" , ctx , uint32 (100 )).Return (
1379+ & model.Block {Height : 100 , Header : commonAncestorHeader }, nil )
1380+
1381+ // Call getNextBlockToProcess - should trigger reorg recovery
1382+ block , err := server .getNextBlockToProcess (ctx )
1383+
1384+ // Should return nil (to retry on next iteration)
1385+ require .NoError (t , err )
1386+ assert .Nil (t , block , "Should return nil after recovery to trigger retry" )
1387+
1388+ // Verify state was rolled back to common ancestor (height 100)
1389+ height , hash , err = server .state .GetLastPersistedBlock ()
12591390 require .NoError (t , err )
1260- assert .Nil (t , block , "Should return nil block when reorg is detected" )
1391+ require .Equal (t , uint32 (100 ), height )
1392+ require .Equal (t , commonAncestorHeader .Hash ().String (), hash .String ())
12611393
12621394 // Verify mock expectations
12631395 mockClient .AssertExpectations (t )
0 commit comments