Skip to content

Commit 6c9010c

Browse files
committed
fix: handle burn chain flapping
This test (from Jude) can reproduce the problematic behavior when the burnchain flaps between two branches. - We get blocks at height 211 - 213 - Then we get a fork, with different blocks at 211-213, as well as 214 and 215. - We then flap back to the original fork, so it goes back to the common ancestor, 210, and tries to download 211-215 - When it gets block 211, it is already in the database, so it fails, cancelling the download of the rest of the blocks, leaving 214 and 215 in this branch not stored - Then we try to store 216, but its parent 215 is not stored yet, so we cannot continue. The fix for this is to ignore attempts to store duplicate blocks.
1 parent 0ccffa8 commit 6c9010c

File tree

5 files changed

+183
-9
lines changed

5 files changed

+183
-9
lines changed

stackslib/src/burnchains/bitcoin/indexer.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,11 @@ impl BitcoinIndexer {
837837
}
838838
} else {
839839
// ignore the reorg
840-
test_debug!("Reorg chain does not overtake original Bitcoin chain");
840+
test_debug!(
841+
"Reorg chain does not overtake original Bitcoin chain ({} >= {})",
842+
orig_total_work,
843+
reorg_total_work
844+
);
841845
new_tip = orig_spv_client.get_headers_height()?;
842846
}
843847
}

stackslib/src/burnchains/db.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ impl<'a> BurnchainDBTransaction<'a> {
313313
&self,
314314
header: &BurnchainBlockHeader,
315315
) -> Result<i64, BurnchainError> {
316-
let sql = "INSERT INTO burnchain_db_block_headers
316+
let sql = "INSERT OR IGNORE INTO burnchain_db_block_headers
317317
(block_height, block_hash, parent_block_hash, num_txs, timestamp)
318318
VALUES (?, ?, ?, ?, ?)";
319319
let args: &[&dyn ToSql] = &[
@@ -323,9 +323,17 @@ impl<'a> BurnchainDBTransaction<'a> {
323323
&u64_to_sql(header.num_txs)?,
324324
&u64_to_sql(header.timestamp)?,
325325
];
326-
match self.sql_tx.execute(sql, args) {
327-
Ok(_) => Ok(self.sql_tx.last_insert_rowid()),
328-
Err(e) => Err(e.into()),
326+
let affected_rows = self.sql_tx.execute(sql, args)?;
327+
if affected_rows == 0 {
328+
// This means a duplicate entry was found and the insert operation was ignored
329+
debug!(
330+
"Duplicate entry for block_hash: {}, insert operation ignored.",
331+
header.block_hash
332+
);
333+
Ok(-1)
334+
} else {
335+
// A new row was inserted successfully
336+
Ok(self.sql_tx.last_insert_rowid())
329337
}
330338
}
331339

stackslib/src/chainstate/coordinator/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2216,7 +2216,7 @@ impl<
22162216
BurnchainDB::get_burnchain_block(&self.burnchain_blocks_db.conn(), &cursor)
22172217
.map_err(|e| {
22182218
warn!(
2219-
"ChainsCoordinator: could not retrieve block burnhash={}",
2219+
"ChainsCoordinator: could not retrieve block burnhash={}",
22202220
&cursor
22212221
);
22222222
Error::NonContiguousBurnchainBlock(e)

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use crate::helium::RunLoop;
1616
use crate::tests::to_addr;
1717
use crate::Config;
1818

19+
#[derive(Debug)]
1920
pub enum BitcoinCoreError {
2021
SpawnFailed(String),
2122
}
@@ -75,7 +76,6 @@ impl BitcoinCoreController {
7576
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{:?}", e))),
7677
};
7778

78-
eprintln!("bitcoind spawned, waiting for startup");
7979
let mut out_reader = BufReader::new(process.stdout.take().unwrap());
8080

8181
let mut line = String::new();
@@ -97,6 +97,34 @@ impl BitcoinCoreController {
9797
Ok(())
9898
}
9999

100+
pub fn stop_bitcoind(&mut self) -> Result<(), BitcoinCoreError> {
101+
if let Some(_) = self.bitcoind_process.take() {
102+
let mut command = Command::new("bitcoin-cli");
103+
command
104+
.stdout(Stdio::piped())
105+
.arg("-rpcconnect=127.0.0.1")
106+
.arg("-rpcport=8332")
107+
.arg("-rpcuser=neon-tester")
108+
.arg("-rpcpassword=neon-tester-pass")
109+
.arg("stop");
110+
111+
let mut process = match command.spawn() {
112+
Ok(child) => child,
113+
Err(e) => return Err(BitcoinCoreError::SpawnFailed(format!("{:?}", e))),
114+
};
115+
116+
let mut out_reader = BufReader::new(process.stdout.take().unwrap());
117+
let mut line = String::new();
118+
while let Ok(bytes_read) = out_reader.read_line(&mut line) {
119+
if bytes_read == 0 {
120+
break;
121+
}
122+
eprintln!("{}", &line);
123+
}
124+
}
125+
Ok(())
126+
}
127+
100128
pub fn kill_bitcoind(&mut self) {
101129
if let Some(mut bitcoind_process) = self.bitcoind_process.take() {
102130
bitcoind_process.kill().unwrap();

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

Lines changed: 136 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::path::Path;
44
use std::sync::atomic::{AtomicU64, Ordering};
55
use std::sync::{mpsc, Arc};
66
use std::time::{Duration, Instant};
7-
use std::{cmp, env, fs, thread};
7+
use std::{cmp, env, fs, io, thread};
88

99
use clarity::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER;
1010
use clarity::vm::ast::ASTRules;
@@ -9302,7 +9302,11 @@ fn test_problematic_blocks_are_not_relayed_or_stored() {
93029302
let tip_info = get_chain_info(&conf);
93039303

93049304
// all blocks were processed
9305-
assert!(tip_info.stacks_tip_height >= old_tip_info.stacks_tip_height + 5);
9305+
info!(
9306+
"tip_info.stacks_tip_height = {}, old_tip_info.stacks_tip_height = {}",
9307+
tip_info.stacks_tip_height, old_tip_info.stacks_tip_height
9308+
);
9309+
assert!(tip_info.stacks_tip_height > old_tip_info.stacks_tip_height);
93069310
// one was problematic -- i.e. the one that included tx_high
93079311
assert_eq!(all_new_files.len(), 1);
93089312

@@ -11174,3 +11178,133 @@ fn filter_txs_by_origin() {
1117411178

1117511179
test_observer::clear();
1117611180
}
11181+
11182+
// https://stackoverflow.com/questions/26958489/how-to-copy-a-folder-recursively-in-rust
11183+
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
11184+
fs::create_dir_all(&dst)?;
11185+
for entry in fs::read_dir(src)? {
11186+
let entry = entry?;
11187+
let ty = entry.file_type()?;
11188+
if ty.is_dir() {
11189+
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
11190+
} else {
11191+
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
11192+
}
11193+
}
11194+
Ok(())
11195+
}
11196+
11197+
#[test]
11198+
#[ignore]
11199+
fn bitcoin_reorg_flap() {
11200+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
11201+
return;
11202+
}
11203+
11204+
let (conf, _miner_account) = neon_integration_test_conf();
11205+
11206+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
11207+
btcd_controller
11208+
.start_bitcoind()
11209+
.map_err(|_e| ())
11210+
.expect("Failed starting bitcoind");
11211+
11212+
let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
11213+
11214+
btc_regtest_controller.bootstrap_chain(201);
11215+
11216+
eprintln!("Chain bootstrapped...");
11217+
11218+
let mut run_loop = neon::RunLoop::new(conf.clone());
11219+
let blocks_processed = run_loop.get_blocks_processed_arc();
11220+
11221+
let channel = run_loop.get_coordinator_channel().unwrap();
11222+
11223+
thread::spawn(move || run_loop.start(None, 0));
11224+
11225+
// give the run loop some time to start up!
11226+
wait_for_runloop(&blocks_processed);
11227+
11228+
// first block wakes up the run loop
11229+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
11230+
11231+
// first block will hold our VRF registration
11232+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
11233+
11234+
let mut sort_height = channel.get_sortitions_processed();
11235+
eprintln!("Sort height: {}", sort_height);
11236+
11237+
while sort_height < 210 {
11238+
next_block_and_wait(&mut btc_regtest_controller, &blocks_processed);
11239+
sort_height = channel.get_sortitions_processed();
11240+
eprintln!("Sort height: {}", sort_height);
11241+
}
11242+
11243+
// stop bitcoind and copy its DB to simulate a chain flap
11244+
btcd_controller.stop_bitcoind().unwrap();
11245+
thread::sleep(Duration::from_secs(5));
11246+
11247+
let btcd_dir = conf.get_burnchain_path_str();
11248+
let mut new_conf = conf.clone();
11249+
new_conf.node.working_dir = format!("{}.new", &conf.node.working_dir);
11250+
fs::create_dir_all(&new_conf.node.working_dir).unwrap();
11251+
11252+
copy_dir_all(&btcd_dir, &new_conf.get_burnchain_path_str()).unwrap();
11253+
11254+
// resume
11255+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
11256+
btcd_controller
11257+
.start_bitcoind()
11258+
.map_err(|_e| ())
11259+
.expect("Failed starting bitcoind");
11260+
11261+
let btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
11262+
thread::sleep(Duration::from_secs(5));
11263+
11264+
info!("\n\nBegin fork A\n\n");
11265+
11266+
// make fork A
11267+
for _i in 0..3 {
11268+
btc_regtest_controller.build_next_block(1);
11269+
thread::sleep(Duration::from_secs(5));
11270+
}
11271+
11272+
btcd_controller.stop_bitcoind().unwrap();
11273+
11274+
info!("\n\nBegin reorg flap from A to B\n\n");
11275+
11276+
// carry out the flap to fork B -- new_conf's state was the same as before the reorg
11277+
let mut btcd_controller = BitcoinCoreController::new(new_conf.clone());
11278+
let btc_regtest_controller = BitcoinRegtestController::new(new_conf.clone(), None);
11279+
11280+
btcd_controller
11281+
.start_bitcoind()
11282+
.map_err(|_e| ())
11283+
.expect("Failed starting bitcoind");
11284+
11285+
for _i in 0..5 {
11286+
btc_regtest_controller.build_next_block(1);
11287+
thread::sleep(Duration::from_secs(5));
11288+
}
11289+
11290+
btcd_controller.stop_bitcoind().unwrap();
11291+
11292+
info!("\n\nBegin reorg flap from B to A\n\n");
11293+
11294+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
11295+
let btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
11296+
btcd_controller
11297+
.start_bitcoind()
11298+
.map_err(|_e| ())
11299+
.expect("Failed starting bitcoind");
11300+
11301+
// carry out the flap back to fork A
11302+
for _i in 0..7 {
11303+
btc_regtest_controller.build_next_block(1);
11304+
thread::sleep(Duration::from_secs(5));
11305+
}
11306+
11307+
assert_eq!(channel.get_sortitions_processed(), 225);
11308+
btcd_controller.stop_bitcoind().unwrap();
11309+
channel.stop_chains_coordinator();
11310+
}

0 commit comments

Comments
 (0)