Skip to content

Commit 2fc6b5c

Browse files
authored
Merge pull request #4673 from stacks-network/hotfix/distinct-burnchain-ops
Hotfix/distinct burnchain ops
2 parents fa0c617 + 017fef5 commit 2fc6b5c

File tree

4 files changed

+222
-7
lines changed

4 files changed

+222
-7
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ jobs:
9090
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
9191
# Do not run this one until we figure out why it fails in CI
9292
# - tests::neon_integrations::bitcoin_reorg_flap
93+
# - tests::neon_integrations::bitcoin_reorg_flap_with_follower
9394
steps:
9495
## Setup test environment
9596
- name: Setup Test Environment

stackslib/src/burnchains/affirmation.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -788,7 +788,14 @@ fn inner_find_heaviest_block_commit_ptr(
788788
}
789789

790790
if let Some(last_vtxindex) = last_vtxindex.as_mut() {
791-
assert!(*last_vtxindex < opdata.vtxindex);
791+
assert!(
792+
*last_vtxindex < opdata.vtxindex,
793+
"{} !< {} at block {} (op {:?})",
794+
*last_vtxindex,
795+
opdata.vtxindex,
796+
opdata.block_height,
797+
&opdata
798+
);
792799
*last_vtxindex = opdata.vtxindex;
793800
} else {
794801
last_vtxindex = Some(opdata.vtxindex);

stackslib/src/burnchains/db.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ CREATE TABLE burnchain_db_block_ops (
232232
-- 32-byte transaction ID
233233
txid TEXT NOT NULL,
234234
235+
-- This should have been present when we created this table, but we forgot.
236+
-- So instead, query methods against this table need to use REPLACE INTO and
237+
-- SELECT DISTINCT for compatibility.
238+
-- PRIMARY KEY(txid,block_hash),
239+
235240
-- ensure that the operation corresponds to an actual block
236241
FOREIGN KEY(block_hash) REFERENCES burnchain_db_block_headers(block_hash)
237242
);
@@ -432,7 +437,8 @@ impl<'a> BurnchainDBTransaction<'a> {
432437
) -> Result<(), BurnchainError> {
433438
// find all block-commits for this block
434439
let commits: Vec<LeaderBlockCommitOp> = {
435-
let block_ops_qry = "SELECT * FROM burnchain_db_block_ops WHERE block_hash = ?";
440+
let block_ops_qry =
441+
"SELECT DISTINCT * FROM burnchain_db_block_ops WHERE block_hash = ?";
436442
let block_ops = query_rows(&self.sql_tx, block_ops_qry, &[&hdr.block_hash])?;
437443
block_ops
438444
.into_iter()
@@ -891,7 +897,7 @@ impl<'a> BurnchainDBTransaction<'a> {
891897
block_header: &BurnchainBlockHeader,
892898
block_ops: &[BlockstackOperationType],
893899
) -> Result<(), BurnchainError> {
894-
let sql = "INSERT INTO burnchain_db_block_ops
900+
let sql = "REPLACE INTO burnchain_db_block_ops
895901
(block_hash, txid, op) VALUES (?, ?, ?)";
896902
let mut stmt = self.sql_tx.prepare(sql)?;
897903
for op in block_ops.iter() {
@@ -1133,7 +1139,7 @@ impl BurnchainDB {
11331139
) -> Result<BurnchainBlockData, BurnchainError> {
11341140
let block_header_qry =
11351141
"SELECT * FROM burnchain_db_block_headers WHERE block_hash = ? LIMIT 1";
1136-
let block_ops_qry = "SELECT * FROM burnchain_db_block_ops WHERE block_hash = ?";
1142+
let block_ops_qry = "SELECT DISTINCT * FROM burnchain_db_block_ops WHERE block_hash = ?";
11371143

11381144
let block_header = query_row(conn, block_header_qry, &[block])?
11391145
.ok_or_else(|| BurnchainError::UnknownBlock(block.clone()))?;
@@ -1150,7 +1156,8 @@ impl BurnchainDB {
11501156
burn_header_hash: &BurnchainHeaderHash,
11511157
txid: &Txid,
11521158
) -> Option<BlockstackOperationType> {
1153-
let qry = "SELECT op FROM burnchain_db_block_ops WHERE txid = ?1 AND block_hash = ?2";
1159+
let qry =
1160+
"SELECT DISTINCT op FROM burnchain_db_block_ops WHERE txid = ?1 AND block_hash = ?2";
11541161
let args: &[&dyn ToSql] = &[txid, burn_header_hash];
11551162

11561163
match query_row(conn, qry, args) {
@@ -1169,7 +1176,7 @@ impl BurnchainDB {
11691176
indexer: &B,
11701177
txid: &Txid,
11711178
) -> Option<BlockstackOperationType> {
1172-
let qry = "SELECT op FROM burnchain_db_block_ops WHERE txid = ?1";
1179+
let qry = "SELECT DISTINCT op FROM burnchain_db_block_ops WHERE txid = ?1";
11731180
let args: &[&dyn ToSql] = &[txid];
11741181

11751182
let ops: Vec<BlockstackOperationType> =

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

Lines changed: 201 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use clarity::vm::ast::ASTRules;
1010
use clarity::vm::costs::ExecutionCost;
1111
use clarity::vm::types::PrincipalData;
1212
use clarity::vm::{ClarityName, ClarityVersion, ContractName, Value, MAX_CALL_STACK_DEPTH};
13-
use rand::Rng;
13+
use rand::{Rng, RngCore};
1414
use rusqlite::types::ToSql;
1515
use serde_json::json;
1616
use stacks::burnchains::bitcoin::address::{BitcoinAddress, LegacyBitcoinAddressType};
@@ -42,6 +42,7 @@ use stacks::core::{
4242
BLOCK_LIMIT_MAINNET_21, CHAIN_ID_TESTNET, HELIUM_BLOCK_LIMIT_20, PEER_VERSION_EPOCH_1_0,
4343
PEER_VERSION_EPOCH_2_0, PEER_VERSION_EPOCH_2_05, PEER_VERSION_EPOCH_2_1,
4444
PEER_VERSION_EPOCH_2_2, PEER_VERSION_EPOCH_2_3, PEER_VERSION_EPOCH_2_4, PEER_VERSION_EPOCH_2_5,
45+
PEER_VERSION_TESTNET,
4546
};
4647
use stacks::net::api::getaccount::AccountEntryResponse;
4748
use stacks::net::api::getcontractsrc::ContractSrcResponse;
@@ -12259,3 +12260,202 @@ fn bitcoin_reorg_flap() {
1225912260
btcd_controller.stop_bitcoind().unwrap();
1226012261
channel.stop_chains_coordinator();
1226112262
}
12263+
12264+
fn next_block_and_wait_all(
12265+
btc_controller: &mut BitcoinRegtestController,
12266+
miner_blocks_processed: &Arc<AtomicU64>,
12267+
follower_blocks_processed: &[&Arc<AtomicU64>],
12268+
) -> bool {
12269+
let followers_current: Vec<_> = follower_blocks_processed
12270+
.iter()
12271+
.map(|blocks_processed| blocks_processed.load(Ordering::SeqCst))
12272+
.collect();
12273+
12274+
if !next_block_and_wait(btc_controller, miner_blocks_processed) {
12275+
return false;
12276+
}
12277+
12278+
// wait for followers to catch up
12279+
loop {
12280+
let finished = follower_blocks_processed
12281+
.iter()
12282+
.zip(followers_current.iter())
12283+
.map(|(blocks_processed, current)| blocks_processed.load(Ordering::SeqCst) <= *current)
12284+
.fold(true, |acc, loaded| acc && loaded);
12285+
12286+
if finished {
12287+
break;
12288+
}
12289+
12290+
thread::sleep(Duration::from_millis(100));
12291+
}
12292+
12293+
true
12294+
}
12295+
12296+
#[test]
12297+
#[ignore]
12298+
fn bitcoin_reorg_flap_with_follower() {
12299+
if env::var("BITCOIND_TEST") != Ok("1".into()) {
12300+
return;
12301+
}
12302+
12303+
let (conf, _miner_account) = neon_integration_test_conf();
12304+
12305+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
12306+
btcd_controller
12307+
.start_bitcoind()
12308+
.expect("Failed starting bitcoind");
12309+
12310+
let mut btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
12311+
12312+
btc_regtest_controller.bootstrap_chain(201);
12313+
12314+
eprintln!("Chain bootstrapped...");
12315+
12316+
let mut miner_run_loop = neon::RunLoop::new(conf.clone());
12317+
let miner_blocks_processed = miner_run_loop.get_blocks_processed_arc();
12318+
let miner_channel = miner_run_loop.get_coordinator_channel().unwrap();
12319+
12320+
let mut follower_conf = conf.clone();
12321+
follower_conf.events_observers.clear();
12322+
follower_conf.node.working_dir = format!("{}-follower", &conf.node.working_dir);
12323+
follower_conf.node.seed = vec![0x01; 32];
12324+
follower_conf.node.local_peer_seed = vec![0x02; 32];
12325+
12326+
let mut rng = rand::thread_rng();
12327+
let mut buf = [0u8; 8];
12328+
rng.fill_bytes(&mut buf);
12329+
12330+
let rpc_port = u16::from_be_bytes(buf[0..2].try_into().unwrap()).saturating_add(1025) - 1; // use a non-privileged port between 1024 and 65534
12331+
let p2p_port = u16::from_be_bytes(buf[2..4].try_into().unwrap()).saturating_add(1025) - 1; // use a non-privileged port between 1024 and 65534
12332+
12333+
let localhost = "127.0.0.1";
12334+
follower_conf.node.rpc_bind = format!("{}:{}", &localhost, rpc_port);
12335+
follower_conf.node.p2p_bind = format!("{}:{}", &localhost, p2p_port);
12336+
follower_conf.node.data_url = format!("http://{}:{}", &localhost, rpc_port);
12337+
follower_conf.node.p2p_address = format!("{}:{}", &localhost, p2p_port);
12338+
12339+
thread::spawn(move || miner_run_loop.start(None, 0));
12340+
wait_for_runloop(&miner_blocks_processed);
12341+
12342+
// figure out the started node's port
12343+
let node_info = get_chain_info(&conf);
12344+
follower_conf.node.add_bootstrap_node(
12345+
&format!(
12346+
"{}@{}",
12347+
&node_info.node_public_key.unwrap(),
12348+
conf.node.p2p_bind
12349+
),
12350+
CHAIN_ID_TESTNET,
12351+
PEER_VERSION_TESTNET,
12352+
);
12353+
12354+
let mut follower_run_loop = neon::RunLoop::new(follower_conf.clone());
12355+
let follower_blocks_processed = follower_run_loop.get_blocks_processed_arc();
12356+
let follower_channel = follower_run_loop.get_coordinator_channel().unwrap();
12357+
12358+
thread::spawn(move || follower_run_loop.start(None, 0));
12359+
wait_for_runloop(&follower_blocks_processed);
12360+
12361+
eprintln!("Follower bootup complete!");
12362+
12363+
// first block wakes up the run loop
12364+
next_block_and_wait_all(&mut btc_regtest_controller, &miner_blocks_processed, &[]);
12365+
12366+
// first block will hold our VRF registration
12367+
next_block_and_wait_all(
12368+
&mut btc_regtest_controller,
12369+
&miner_blocks_processed,
12370+
&[&follower_blocks_processed],
12371+
);
12372+
12373+
let mut miner_sort_height = miner_channel.get_sortitions_processed();
12374+
let mut follower_sort_height = follower_channel.get_sortitions_processed();
12375+
eprintln!(
12376+
"Miner sort height: {}, follower sort height: {}",
12377+
miner_sort_height, follower_sort_height
12378+
);
12379+
12380+
while miner_sort_height < 210 && follower_sort_height < 210 {
12381+
next_block_and_wait_all(
12382+
&mut btc_regtest_controller,
12383+
&miner_blocks_processed,
12384+
&[&follower_blocks_processed],
12385+
);
12386+
miner_sort_height = miner_channel.get_sortitions_processed();
12387+
follower_sort_height = miner_channel.get_sortitions_processed();
12388+
eprintln!(
12389+
"Miner sort height: {}, follower sort height: {}",
12390+
miner_sort_height, follower_sort_height
12391+
);
12392+
}
12393+
12394+
// stop bitcoind and copy its DB to simulate a chain flap
12395+
btcd_controller.stop_bitcoind().unwrap();
12396+
thread::sleep(Duration::from_secs(5));
12397+
12398+
let btcd_dir = conf.get_burnchain_path_str();
12399+
let mut new_conf = conf.clone();
12400+
new_conf.node.working_dir = format!("{}.new", &conf.node.working_dir);
12401+
fs::create_dir_all(&new_conf.node.working_dir).unwrap();
12402+
12403+
copy_dir_all(&btcd_dir, &new_conf.get_burnchain_path_str()).unwrap();
12404+
12405+
// resume
12406+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
12407+
btcd_controller
12408+
.start_bitcoind()
12409+
.expect("Failed starting bitcoind");
12410+
12411+
let btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
12412+
thread::sleep(Duration::from_secs(5));
12413+
12414+
info!("\n\nBegin fork A\n\n");
12415+
12416+
// make fork A
12417+
for _i in 0..3 {
12418+
btc_regtest_controller.build_next_block(1);
12419+
thread::sleep(Duration::from_secs(5));
12420+
}
12421+
12422+
btcd_controller.stop_bitcoind().unwrap();
12423+
12424+
info!("\n\nBegin reorg flap from A to B\n\n");
12425+
12426+
// carry out the flap to fork B -- new_conf's state was the same as before the reorg
12427+
let mut btcd_controller = BitcoinCoreController::new(new_conf.clone());
12428+
let btc_regtest_controller = BitcoinRegtestController::new(new_conf.clone(), None);
12429+
12430+
btcd_controller
12431+
.start_bitcoind()
12432+
.expect("Failed starting bitcoind");
12433+
12434+
for _i in 0..5 {
12435+
btc_regtest_controller.build_next_block(1);
12436+
thread::sleep(Duration::from_secs(5));
12437+
}
12438+
12439+
btcd_controller.stop_bitcoind().unwrap();
12440+
12441+
info!("\n\nBegin reorg flap from B to A\n\n");
12442+
12443+
let mut btcd_controller = BitcoinCoreController::new(conf.clone());
12444+
let btc_regtest_controller = BitcoinRegtestController::new(conf.clone(), None);
12445+
btcd_controller
12446+
.start_bitcoind()
12447+
.expect("Failed starting bitcoind");
12448+
12449+
// carry out the flap back to fork A
12450+
for _i in 0..7 {
12451+
btc_regtest_controller.build_next_block(1);
12452+
thread::sleep(Duration::from_secs(5));
12453+
}
12454+
12455+
assert_eq!(miner_channel.get_sortitions_processed(), 225);
12456+
assert_eq!(follower_channel.get_sortitions_processed(), 225);
12457+
12458+
btcd_controller.stop_bitcoind().unwrap();
12459+
miner_channel.stop_chains_coordinator();
12460+
follower_channel.stop_chains_coordinator();
12461+
}

0 commit comments

Comments
 (0)