Skip to content

Commit f47addd

Browse files
committed
fix(bitcoind_rpc): emit invalidated heights when start_height is above agreement point
When a reorg drops the agreement point below `start_height`, the emitter would skip directly to `start_height`, producing a checkpoint that could not connect with the caller's local chain (`CannotConnectError`). Fix: override `start_height` to the agreement height when a reorg is detected (agreement point below both `start_height` and `last_cp`), so the emitter revisits the invalidated block heights.
1 parent de7a89f commit f47addd

2 files changed

Lines changed: 73 additions & 28 deletions

File tree

crates/bitcoind_rpc/src/lib.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ where
282282
let client = &*emitter.client;
283283

284284
if let Some(last_res) = &emitter.last_block {
285-
let next_hash = if last_res.height < emitter.start_height as _ {
285+
let next_hash = if last_res.height + 1 < emitter.start_height as _ {
286286
// enforce start height
287287
let next_hash = client.get_block_hash(emitter.start_height as _)?;
288288
// make sure last emission is still in best chain
@@ -362,6 +362,13 @@ where
362362
continue;
363363
}
364364
PollResponse::AgreementFound(res, cp) => {
365+
// When a reorg happens, the agreement point drops below `last_cp`. We
366+
// override `start_height` so the emitter revisits the invalidated heights.
367+
if (res.height as u32) < emitter.start_height
368+
&& (res.height as u32) < emitter.last_cp.height()
369+
{
370+
emitter.start_height = res.height as _;
371+
}
365372
// get rid of evicted blocks
366373
emitter.last_cp = cp;
367374
emitter.last_block = Some(res);

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,7 @@ fn mempool_avoids_re_emission() -> anyhow::Result<()> {
487487
/// If blockchain re-org includes the start height, emit new start height block
488488
///
489489
/// 1. mine 101 blocks
490-
/// 2. emit blocks 99a, 100a
490+
/// 2. emit blocks 98a, 99a, 100a
491491
/// 3. invalidate blocks 99a, 100a, 101a
492492
/// 4. mine new blocks 99b, 100b, 101b
493493
/// 5. emit block 99b
@@ -505,48 +505,45 @@ fn no_agreement_point() -> anyhow::Result<()> {
505505
let mut emitter = Emitter::new(
506506
&client,
507507
CheckPoint::new(0, env.genesis_hash()?),
508-
(PREMINE_COUNT - 2) as u32,
508+
(PREMINE_COUNT - 3) as u32,
509509
NO_EXPECTED_MEMPOOL_TXS,
510510
);
511511

512512
// mine 101 blocks
513513
env.mine_blocks(PREMINE_COUNT, None)?;
514514

515-
// emit block 99a
516-
let block_header_99a = emitter
517-
.next_block()?
518-
.expect("block 99a header")
519-
.block
520-
.header;
521-
let block_hash_99a = block_header_99a.block_hash();
522-
let block_hash_98a = block_header_99a.prev_blockhash;
523-
524-
// emit block 100a
525-
let block_header_100a = emitter.next_block()?.expect("block 100a header").block;
526-
let block_hash_100a = block_header_100a.block_hash();
515+
// emit blocks: 98a, 99a, 100a
516+
let block_98a = emitter.next_block()?.expect("block 98a");
517+
let block_99a = emitter.next_block()?.expect("block 99a");
518+
let block_100a = emitter.next_block()?.expect("block 100a");
519+
assert_eq!(block_98a.block_height(), 98);
520+
assert_eq!(block_99a.block_height(), 99);
521+
assert_eq!(block_100a.block_height(), 100);
527522

528523
// get hash for block 101a
529-
let block_hash_101a = env.rpc_client().get_block_hash(101)?.block_hash()?;
524+
let blockhash_101a = env.rpc_client().get_block_hash(101)?.block_hash()?;
530525

531526
// invalidate blocks 99a, 100a, 101a
532-
env.rpc_client().invalidate_block(block_hash_99a)?;
533-
env.rpc_client().invalidate_block(block_hash_100a)?;
534-
env.rpc_client().invalidate_block(block_hash_101a)?;
527+
env.rpc_client().invalidate_block(blockhash_101a)?;
528+
env.rpc_client().invalidate_block(block_100a.block_hash())?;
529+
env.rpc_client().invalidate_block(block_99a.block_hash())?;
535530

536531
// mine new blocks 99b, 100b, 101b
537532
env.mine_blocks(3, None)?;
538533

539534
// emit block header 99b
540-
let block_header_99b = emitter
541-
.next_block()?
542-
.expect("block 99b header")
543-
.block
544-
.header;
545-
let block_hash_99b = block_header_99b.block_hash();
546-
let block_hash_98b = block_header_99b.prev_blockhash;
535+
let block_99b = emitter.next_block()?.expect("block 99b");
536+
assert_eq!(block_99b.block_height(), 99);
547537

548-
assert_ne!(block_hash_99a, block_hash_99b);
549-
assert_eq!(block_hash_98a, block_hash_98b);
538+
assert_ne!(block_99a.block_hash(), block_99b.block_hash());
539+
assert_eq!(
540+
block_98a.block_hash(),
541+
block_99a.block.header.prev_blockhash
542+
);
543+
assert_eq!(
544+
block_98a.block_hash(),
545+
block_99b.block.header.prev_blockhash
546+
);
550547

551548
Ok(())
552549
}
@@ -661,6 +658,47 @@ fn test_expect_tx_evicted() -> anyhow::Result<()> {
661658
Ok(())
662659
}
663660

661+
/// Creating a new [`Emitter`] after a reorg with `start_height` at the tip should still
662+
/// produce a connectable checkpoint. When blocks are invalidated, the emitted checkpoint must
663+
/// include the invalidation height so the update can connect with the original chain.
664+
#[test]
665+
fn test_sync_with_new_emitter_after_reorg() -> anyhow::Result<()> {
666+
let env = TestEnv::new()?;
667+
let (mut local_chain, _) = LocalChain::from_genesis(env.genesis_hash()?);
668+
let client = ClientExt::get_rpc_client(&env)?;
669+
670+
env.mine_blocks(110, None)?;
671+
672+
let mut emitter = Emitter::new(&client, local_chain.tip(), 0, NO_EXPECTED_MEMPOOL_TXS);
673+
while let Some(emission) = emitter.next_block()? {
674+
let _ = local_chain.apply_update(emission.checkpoint)?;
675+
}
676+
677+
let pre_reorg_tip = local_chain.tip();
678+
let tip_height = pre_reorg_tip.height();
679+
680+
env.reorg(6)?;
681+
682+
// New emitter with start_height = tip height (common caller pattern).
683+
let mut emitter = Emitter::new(
684+
&client,
685+
local_chain.tip(),
686+
tip_height,
687+
NO_EXPECTED_MEMPOOL_TXS,
688+
);
689+
690+
while let Some(emission) = emitter.next_block()? {
691+
let _ = local_chain
692+
.apply_update(emission.checkpoint)
693+
.expect("emission checkpoint must connect with local chain");
694+
}
695+
696+
assert_eq!(local_chain.tip().height(), tip_height);
697+
assert_ne!(local_chain.tip().hash(), pre_reorg_tip.hash());
698+
699+
Ok(())
700+
}
701+
664702
#[test]
665703
fn detect_new_mempool_txs() -> anyhow::Result<()> {
666704
let env = TestEnv::new()?;

0 commit comments

Comments
 (0)