Skip to content

Commit d18b0fb

Browse files
authored
Merge pull request #2773 from blockstack/fix/2771
Fix: 2771
2 parents 9390b29 + 5598649 commit d18b0fb

File tree

3 files changed

+120
-11
lines changed

3 files changed

+120
-11
lines changed

.github/actions/bitcoin-int-tests/Dockerfile.bitcoin-tests

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ RUN cargo test -- --test-threads 1 --ignored tests::neon_integrations::bitcoind_
2121
RUN cargo test -- --test-threads 1 --ignored tests::neon_integrations::liquid_ustx_integration
2222
RUN cargo test -- --test-threads 1 --ignored tests::neon_integrations::stx_transfer_btc_integration_test
2323
RUN cargo test -- --test-threads 1 --ignored tests::neon_integrations::bitcoind_forking_test
24+
RUN cargo test -- --test-threads 1 --ignored tests::neon_integrations::should_fix_2771
2425
RUN cargo test -- --test-threads 1 --ignored tests::neon_integrations::pox_integration_test
2526
RUN cargo test -- --test-threads 1 --ignored tests::bitcoin_regtest::bitcoind_integration_test
2627
RUN cargo test -- --test-threads 1 --ignored tests::should_succeed_handling_malformed_and_valid_txs

src/burnchains/burnchain.rs

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::sync::{
2525
Arc,
2626
};
2727
use std::thread;
28-
use std::time::Instant;
28+
use std::time::{Duration, Instant};
2929

3030
use crate::types::chainstate::StacksAddress;
3131
use crate::types::proof::TrieHash;
@@ -891,8 +891,8 @@ impl Burnchain {
891891
}
892892

893893
/// Determine if there has been a chain reorg, given our current canonical burnchain tip.
894-
/// Return the new chain tip
895-
fn sync_reorg<I: BurnchainIndexer>(indexer: &mut I) -> Result<u64, burnchain_error> {
894+
/// Return the new chain tip and a boolean signaling the presence of a reorg
895+
fn sync_reorg<I: BurnchainIndexer>(indexer: &mut I) -> Result<(u64, bool), burnchain_error> {
896896
let headers_path = indexer.get_headers_path();
897897

898898
// sanity check -- what is the height of our highest header
@@ -905,7 +905,7 @@ impl Burnchain {
905905
})?;
906906

907907
if headers_height == 0 {
908-
return Ok(0);
908+
return Ok((0, false));
909909
}
910910

911911
// did we encounter a reorg since last sync? Find the highest common ancestor of the
@@ -921,10 +921,10 @@ impl Burnchain {
921921
"Burnchain reorg detected: highest common ancestor at height {}",
922922
reorg_height
923923
);
924-
return Ok(reorg_height);
924+
return Ok((reorg_height, true));
925925
} else {
926926
// no reorg
927-
return Ok(headers_height);
927+
return Ok((headers_height, false));
928928
}
929929
}
930930

@@ -959,7 +959,7 @@ impl Burnchain {
959959

960960
// handle reorgs
961961
let orig_header_height = indexer.get_headers_height()?; // 1-indexed
962-
let sync_height = Burnchain::sync_reorg(indexer)?;
962+
let (sync_height, _) = Burnchain::sync_reorg(indexer)?;
963963
if sync_height + 1 < orig_header_height {
964964
// a reorg happened
965965
warn!(
@@ -1170,9 +1170,8 @@ impl Burnchain {
11701170
let db_height = burn_chain_tip.block_height;
11711171

11721172
// handle reorgs
1173-
let orig_header_height = indexer.get_headers_height()?; // 1-indexed
1174-
let sync_height = Burnchain::sync_reorg(indexer)?;
1175-
if sync_height + 1 < orig_header_height {
1173+
let (sync_height, did_reorg) = Burnchain::sync_reorg(indexer)?;
1174+
if did_reorg {
11761175
// a reorg happened
11771176
warn!(
11781177
"Dropping headers higher than {} due to burnchain reorg",
@@ -1186,6 +1185,23 @@ impl Burnchain {
11861185

11871186
// fetch all headers, no matter what
11881187
let mut end_block = indexer.sync_headers(sync_height, None)?;
1188+
if did_reorg && sync_height > 0 {
1189+
// a reorg happened, and the last header fetched
1190+
// is on a smaller fork than the one we just
1191+
// invalidated. Wait for more blocks.
1192+
while end_block < db_height {
1193+
if let Some(ref should_keep_running) = should_keep_running {
1194+
if !should_keep_running.load(Ordering::SeqCst) {
1195+
return Err(burnchain_error::CoordinatorClosed);
1196+
}
1197+
}
1198+
let end_height = target_block_height_opt.unwrap_or(0).max(db_height);
1199+
info!("Burnchain reorg happened at height {} invalidating chain tip {} but only {} headers presents on canonical chain. Retry in 2s", sync_height, db_height, end_block);
1200+
thread::sleep(Duration::from_millis(2000));
1201+
end_block = indexer.sync_headers(sync_height, Some(end_height))?;
1202+
}
1203+
}
1204+
11891205
let mut start_block = sync_height;
11901206
if db_height < start_block {
11911207
start_block = db_height;
@@ -1327,7 +1343,8 @@ impl Burnchain {
13271343
while let Ok(Some(burnchain_block)) = db_recv.recv() {
13281344
debug!("Try recv next parsed block");
13291345

1330-
if burnchain_block.block_height() == 0 {
1346+
let block_height = burnchain_block.block_height();
1347+
if block_height == 0 {
13311348
continue;
13321349
}
13331350

testnet/stacks-node/src/tests/neon_integrations.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,97 @@ fn bitcoind_forking_test() {
13071307
// but we're able to keep on mining
13081308
assert_eq!(account.nonce, 3);
13091309

1310+
// Let's create another fork, deeper
1311+
let burn_header_hash_to_fork = btc_regtest_controller.get_block_hash(206);
1312+
btc_regtest_controller.invalidate_block(&burn_header_hash_to_fork);
1313+
btc_regtest_controller.build_next_block(10);
1314+
1315+
thread::sleep(Duration::from_secs(5));
1316+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
1317+
1318+
let account = get_account(&http_origin, &miner_account);
1319+
assert_eq!(account.balance, 0);
1320+
assert_eq!(account.nonce, 3);
1321+
1322+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
1323+
1324+
let account = get_account(&http_origin, &miner_account);
1325+
assert_eq!(account.balance, 0);
1326+
// but we're able to keep on mining
1327+
assert_eq!(account.nonce, 3);
1328+
1329+
channel.stop_chains_coordinator();
1330+
}
1331+
1332+
#[test]
1333+
#[ignore]
1334+
fn should_fix_2771() {
1335+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
1336+
return;
1337+
}
1338+
1339+
let (conf, miner_account) = neon_integration_test_conf();
1340+
1341+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
1342+
btcd_controller
1343+
.start_bitcoind()
1344+
.map_err(|_e| ())
1345+
.expect("Failed starting bitcoind");
1346+
1347+
let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
1348+
1349+
btc_regtest_controller.bootstrap_chain(201);
1350+
1351+
eprintln!("Chain bootstrapped...");
1352+
1353+
let mut run_loop = neon::RunLoop::new(conf);
1354+
let blocks_processed = run_loop.get_blocks_processed_arc();
1355+
1356+
let channel = run_loop.get_coordinator_channel().unwrap();
1357+
1358+
thread::spawn(move || run_loop.start(None, 0));
1359+
1360+
// give the run loop some time to start up!
1361+
wait_for_runloop(&blocks_processed);
1362+
1363+
// first block wakes up the run loop
1364+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
1365+
1366+
// first block will hold our VRF registration
1367+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
1368+
1369+
let mut sort_height = channel.get_sortitions_processed();
1370+
eprintln!("Sort height: {}", sort_height);
1371+
1372+
while sort_height < 210 {
1373+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
1374+
sort_height = channel.get_sortitions_processed();
1375+
eprintln!("Sort height: {}", sort_height);
1376+
}
1377+
1378+
// okay, let's figure out the burn block we want to fork away.
1379+
let reorg_height = 208;
1380+
warn!("Will trigger re-org at block {}", reorg_height);
1381+
let burn_header_hash_to_fork = btc_regtest_controller.get_block_hash(reorg_height);
1382+
btc_regtest_controller.invalidate_block(&burn_header_hash_to_fork);
1383+
btc_regtest_controller.build_next_block(1);
1384+
thread::sleep(Duration::from_secs(5));
1385+
1386+
// The test here consists in producing a canonical chain with 210 blocks.
1387+
// Once done, we invalidate the block 208, and instead of rebuilding directly
1388+
// a longer fork with N blocks (as done in the bitcoind_forking_test)
1389+
// we slowly add some more blocks.
1390+
// Without the patch, this behavior ends up crashing the node with errors like:
1391+
// WARN [1626791307.078061] [src/chainstate/coordinator/mod.rs:535] [chains-coordinator] ChainsCoordinator: could not retrieve block burnhash=40bdbf0dda349642bdf4dd30dd31af4f0c9979ce12a7c17485245d0a6ddd970b
1392+
// WARN [1626791307.078098] [src/chainstate/coordinator/mod.rs:308] [chains-coordinator] Error processing new burn block: NonContiguousBurnchainBlock(UnknownBlock(40bdbf0dda349642bdf4dd30dd31af4f0c9979ce12a7c17485245d0a6ddd970b))
1393+
// And the burnchain db ends up in the same state we ended up while investigating 2771.
1394+
// With this patch, the node is able to entirely register this new canonical fork, and then able to make progress and finish successfully.
1395+
while sort_height < 213 {
1396+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
1397+
sort_height = channel.get_sortitions_processed();
1398+
eprintln!("Sort height: {}", sort_height);
1399+
}
1400+
13101401
channel.stop_chains_coordinator();
13111402
}
13121403

0 commit comments

Comments
 (0)