Skip to content

Commit 1e60445

Browse files
authored
Merge pull request #6214 from kantai/fix/error-during-btc-reorg
fix: #6213 handle downloader errors in btc reorg logic
2 parents 2742093 + ef938b3 commit 1e60445

File tree

5 files changed

+251
-29
lines changed

5 files changed

+251
-29
lines changed

stackslib/src/burnchains/burnchain.rs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,19 @@ use crate::core::{
6262
use crate::monitoring::update_burnchain_height;
6363
use crate::util_lib::db::Error as db_error;
6464

65+
#[cfg(any(test, feature = "testing"))]
66+
pub static TEST_DOWNLOAD_ERROR_ON_REORG: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
67+
68+
#[cfg(any(test, feature = "testing"))]
69+
fn fault_inject_downloader_on_reorg(did_reorg: bool) -> bool {
70+
did_reorg && *TEST_DOWNLOAD_ERROR_ON_REORG.lock().unwrap()
71+
}
72+
73+
#[cfg(not(any(test, feature = "testing")))]
74+
fn fault_inject_downloader_on_reorg(_did_reorg: bool) -> bool {
75+
false
76+
}
77+
6578
impl BurnchainStateTransitionOps {
6679
pub fn noop() -> BurnchainStateTransitionOps {
6780
BurnchainStateTransitionOps {
@@ -1502,10 +1515,38 @@ impl Burnchain {
15021515
}
15031516
}
15041517

1505-
let mut start_block = sync_height;
1506-
if db_height < start_block {
1507-
start_block = db_height;
1508-
}
1518+
// check if the db has the parent of sync_height, if not,
1519+
// start at the highest common ancestor
1520+
// if it does, then start at the minimum of db_height and sync_height
1521+
let start_block = if sync_height == 0 {
1522+
0
1523+
} else {
1524+
let Some(sync_header) = indexer.read_burnchain_header(sync_height)? else {
1525+
warn!("Missing burnchain header not read for sync start height";
1526+
"sync_height" => sync_height);
1527+
return Err(burnchain_error::MissingHeaders);
1528+
};
1529+
1530+
let mut cursor = sync_header;
1531+
loop {
1532+
if burnchain_db.has_burnchain_block(&cursor.block_hash)? {
1533+
break cursor.block_height;
1534+
}
1535+
1536+
cursor = indexer
1537+
.read_burnchain_header(cursor.block_height.checked_sub(1).ok_or_else(
1538+
|| {
1539+
error!("Could not find common ancestor, passed bitcoin genesis");
1540+
burnchain_error::MissingHeaders
1541+
},
1542+
)?)?
1543+
.ok_or_else(|| {
1544+
warn!("Missing burnchain header not read for parent of indexed header";
1545+
"indexed_header" => ?cursor);
1546+
burnchain_error::MissingHeaders
1547+
})?;
1548+
}
1549+
};
15091550

15101551
debug!(
15111552
"Sync'ed headers from {} to {}. DB at {}",
@@ -1605,6 +1646,17 @@ impl Burnchain {
16051646
_ => {}
16061647
};
16071648

1649+
if fault_inject_downloader_on_reorg(did_reorg) {
1650+
warn!("Stalling and yielding an error for the reorg";
1651+
"error_ht" => BurnHeaderIPC::height(&ipc_header),
1652+
"sync_ht" => sync_height,
1653+
"start_ht" => start_block,
1654+
"end_ht" => end_block,
1655+
);
1656+
thread::sleep(Duration::from_secs(10));
1657+
return Err(burnchain_error::UnsupportedBurnchain);
1658+
}
1659+
16081660
let download_start = get_epoch_time_ms();
16091661
let ipc_block = downloader.download(&ipc_header)?;
16101662
let download_end = get_epoch_time_ms();

stackslib/src/burnchains/db.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,12 @@ impl BurnchainDB {
11291129
Ok(res.is_some())
11301130
}
11311131

1132+
pub fn has_burnchain_block(&self, block: &BurnchainHeaderHash) -> Result<bool, BurnchainError> {
1133+
let qry = "SELECT 1 FROM burnchain_db_block_headers WHERE block_hash = ?1";
1134+
let res: Option<i64> = query_row(&self.conn, qry, &[block])?;
1135+
Ok(res.is_some())
1136+
}
1137+
11321138
pub fn get_burnchain_header<B: BurnchainHeaderReader>(
11331139
conn: &DBConn,
11341140
indexer: &B,

testnet/stacks-node/src/neon_node.rs

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,8 @@ pub struct RelayerThread {
487487
/// if true, then the last time the miner thread was launched, it was used to mine a Stacks
488488
/// block (used to alternate between mining microblocks and Stacks blocks that confirm them)
489489
mined_stacks_block: bool,
490+
/// if true, the last time the miner thread was launched, it did not mine.
491+
last_attempt_failed: bool,
490492
}
491493

492494
pub(crate) struct BlockMinerThread {
@@ -1005,7 +1007,7 @@ impl BlockMinerThread {
10051007
registered_key,
10061008
burn_block,
10071009
event_dispatcher: rt.event_dispatcher.clone(),
1008-
failed_to_submit_last_attempt: false,
1010+
failed_to_submit_last_attempt: rt.last_attempt_failed,
10091011
}
10101012
}
10111013

@@ -1131,10 +1133,10 @@ impl BlockMinerThread {
11311133

11321134
/// Is a given Stacks staging block on the canonical burnchain fork?
11331135
pub(crate) fn is_on_canonical_burnchain_fork(
1134-
candidate: &StagingBlock,
1136+
candidate_ch: &ConsensusHash,
1137+
candidate_bh: &BlockHeaderHash,
11351138
sortdb_tip_handle: &SortitionHandleConn,
11361139
) -> bool {
1137-
let candidate_ch = &candidate.consensus_hash;
11381140
let candidate_burn_ht = match SortitionDB::get_block_snapshot_consensus(
11391141
sortdb_tip_handle.conn(),
11401142
candidate_ch,
@@ -1143,13 +1145,13 @@ impl BlockMinerThread {
11431145
Ok(None) => {
11441146
warn!("Tried to evaluate potential chain tip with an unknown consensus hash";
11451147
"consensus_hash" => %candidate_ch,
1146-
"stacks_block_hash" => %candidate.anchored_block_hash);
1148+
"stacks_block_hash" => %candidate_bh);
11471149
return false;
11481150
}
11491151
Err(e) => {
11501152
warn!("Error while trying to evaluate potential chain tip with an unknown consensus hash";
11511153
"consensus_hash" => %candidate_ch,
1152-
"stacks_block_hash" => %candidate.anchored_block_hash,
1154+
"stacks_block_hash" => %candidate_bh,
11531155
"err" => ?e);
11541156
return false;
11551157
}
@@ -1159,13 +1161,13 @@ impl BlockMinerThread {
11591161
Ok(None) => {
11601162
warn!("Tried to evaluate potential chain tip with a consensus hash ahead of canonical tip";
11611163
"consensus_hash" => %candidate_ch,
1162-
"stacks_block_hash" => %candidate.anchored_block_hash);
1164+
"stacks_block_hash" => %candidate_bh);
11631165
return false;
11641166
}
11651167
Err(e) => {
11661168
warn!("Error while trying to evaluate potential chain tip with an unknown consensus hash";
11671169
"consensus_hash" => %candidate_ch,
1168-
"stacks_block_hash" => %candidate.anchored_block_hash,
1170+
"stacks_block_hash" => %candidate_bh,
11691171
"err" => ?e);
11701172
return false;
11711173
}
@@ -1202,7 +1204,13 @@ impl BlockMinerThread {
12021204

12031205
let stacks_tips: Vec<_> = stacks_tips
12041206
.into_iter()
1205-
.filter(|candidate| Self::is_on_canonical_burnchain_fork(candidate, &sortdb_tip_handle))
1207+
.filter(|candidate| {
1208+
Self::is_on_canonical_burnchain_fork(
1209+
&candidate.consensus_hash,
1210+
&candidate.anchored_block_hash,
1211+
&sortdb_tip_handle,
1212+
)
1213+
})
12061214
.collect();
12071215

12081216
if stacks_tips.is_empty() {
@@ -1233,7 +1241,11 @@ impl BlockMinerThread {
12331241
.expect("FATAL: could not query chain tips at height")
12341242
.into_iter()
12351243
.filter(|candidate| {
1236-
Self::is_on_canonical_burnchain_fork(candidate, &sortdb_tip_handle)
1244+
Self::is_on_canonical_burnchain_fork(
1245+
&candidate.consensus_hash,
1246+
&candidate.anchored_block_hash,
1247+
&sortdb_tip_handle,
1248+
)
12371249
});
12381250

12391251
for tip in stacks_tips {
@@ -1311,7 +1323,7 @@ impl BlockMinerThread {
13111323
chain_state: &mut StacksChainState,
13121324
at_stacks_height: Option<u64>,
13131325
) -> Option<TipCandidate> {
1314-
info!("Picking best Stacks tip");
1326+
debug!("Picking best Stacks tip");
13151327
let miner_config = config.get_miner_config();
13161328
let max_depth = miner_config.max_reorg_depth;
13171329

@@ -1320,10 +1332,18 @@ impl BlockMinerThread {
13201332
Self::load_candidate_tips(burn_db, chain_state, max_depth, at_stacks_height);
13211333

13221334
let mut previous_best_tips = HashMap::new();
1335+
let sortdb_tip_handle = burn_db.index_handle_at_tip();
13231336
for tip in stacks_tips.iter() {
13241337
let Some(prev_best_tip) = globals.get_best_tip(tip.stacks_height) else {
13251338
continue;
13261339
};
1340+
if !Self::is_on_canonical_burnchain_fork(
1341+
&prev_best_tip.consensus_hash,
1342+
&prev_best_tip.anchored_block_hash,
1343+
&sortdb_tip_handle,
1344+
) {
1345+
continue;
1346+
}
13271347
previous_best_tips.insert(tip.stacks_height, prev_best_tip);
13281348
}
13291349

@@ -1332,7 +1352,7 @@ impl BlockMinerThread {
13321352
globals.add_best_tip(best_tip.stacks_height, best_tip.clone(), max_depth);
13331353
} else {
13341354
// no best-tip found; revert to old tie-breaker logic
1335-
info!("No best-tips found; using old tie-breaking logic");
1355+
debug!("No best-tips found; using old tie-breaking logic");
13361356
return chain_state
13371357
.get_stacks_chain_tip(burn_db)
13381358
.expect("FATAL: could not load chain tip")
@@ -1476,7 +1496,7 @@ impl BlockMinerThread {
14761496
}
14771497
}
14781498

1479-
info!(
1499+
debug!(
14801500
"Tip #{i} {}/{} at {}:{} has score {score} ({})",
14811501
&leaf_tip.consensus_hash,
14821502
&leaf_tip.anchored_block_hash,
@@ -1505,7 +1525,7 @@ impl BlockMinerThread {
15051525
.get(*best_tip_idx)
15061526
.expect("FATAL: candidates should not be empty");
15071527

1508-
info!(
1528+
debug!(
15091529
"Best tip is #{best_tip_idx} {}/{}",
15101530
&best_tip.consensus_hash, &best_tip.anchored_block_hash
15111531
);
@@ -1617,12 +1637,12 @@ impl BlockMinerThread {
16171637
} else {
16181638
let mut best_attempt = 0;
16191639
let mut max_txs = 0;
1620-
info!(
1640+
debug!(
16211641
"Consider {} in-flight Stacks tip(s)",
16221642
&last_mined_blocks.len()
16231643
);
16241644
for prev_block in last_mined_blocks.iter() {
1625-
info!(
1645+
debug!(
16261646
"Consider in-flight block {} on Stacks tip {}/{} in {} with {} txs",
16271647
&prev_block.anchored_block.block_hash(),
16281648
&prev_block.parent_consensus_hash,
@@ -1632,12 +1652,6 @@ impl BlockMinerThread {
16321652
);
16331653
max_txs = cmp::max(max_txs, prev_block.anchored_block.txs.len());
16341654

1635-
if prev_block.anchored_block.txs.len() == 1 && prev_block.attempt == 1 {
1636-
// Don't let the fact that we've built an empty block during this sortition
1637-
// prevent us from trying again.
1638-
best_attempt = 1;
1639-
continue;
1640-
}
16411655
if prev_block.parent_consensus_hash == *parent_consensus_hash
16421656
&& prev_block.burn_hash == self.burn_block.burn_header_hash
16431657
&& prev_block.anchored_block.header.parent_block
@@ -1669,7 +1683,7 @@ impl BlockMinerThread {
16691683
if !force {
16701684
// the chain tip hasn't changed since we attempted to build a block. Use what we
16711685
// already have.
1672-
info!("Relayer: Stacks tip is unchanged since we last tried to mine a block off of {}/{} at height {} with {} txs, in {} at burn height {parent_block_burn_height}, and no new microblocks ({} <= {} + 1)",
1686+
debug!("Relayer: Stacks tip is unchanged since we last tried to mine a block off of {}/{} at height {} with {} txs, in {} at burn height {parent_block_burn_height}, and no new microblocks ({} <= {} + 1)",
16731687
&prev_block.parent_consensus_hash, &prev_block.anchored_block.header.parent_block, prev_block.anchored_block.header.total_work.work,
16741688
prev_block.anchored_block.txs.len(), prev_block.burn_hash, stream.len(), prev_block.anchored_block.header.parent_microblock_sequence);
16751689

@@ -1680,15 +1694,15 @@ impl BlockMinerThread {
16801694
// TODO: only consider rebuilding our anchored block if we (a) have
16811695
// time, and (b) the new microblocks are worth more than the new BTC
16821696
// fee minus the old BTC fee
1683-
info!("Relayer: Stacks tip is unchanged since we last tried to mine a block off of {}/{} at height {} with {} txs, in {} at burn height {parent_block_burn_height}, but there are new microblocks ({} > {} + 1)",
1697+
debug!("Relayer: Stacks tip is unchanged since we last tried to mine a block off of {}/{} at height {} with {} txs, in {} at burn height {parent_block_burn_height}, but there are new microblocks ({} > {} + 1)",
16841698
&prev_block.parent_consensus_hash, &prev_block.anchored_block.header.parent_block, prev_block.anchored_block.header.total_work.work,
16851699
prev_block.anchored_block.txs.len(), prev_block.burn_hash, stream.len(), prev_block.anchored_block.header.parent_microblock_sequence);
16861700

16871701
best_attempt = cmp::max(best_attempt, prev_block.attempt);
16881702
}
16891703
} else if !force {
16901704
// no microblock stream to confirm, and the stacks tip hasn't changed
1691-
info!("Relayer: Stacks tip is unchanged since we last tried to mine a block off of {}/{} at height {} with {} txs, in {} at burn height {parent_block_burn_height}, and no microblocks present",
1705+
debug!("Relayer: Stacks tip is unchanged since we last tried to mine a block off of {}/{} at height {} with {} txs, in {} at burn height {parent_block_burn_height}, and no microblocks present",
16921706
&prev_block.parent_consensus_hash, &prev_block.anchored_block.header.parent_block, prev_block.anchored_block.header.total_work.work,
16931707
prev_block.anchored_block.txs.len(), prev_block.burn_hash);
16941708

@@ -2733,7 +2747,12 @@ impl BlockMinerThread {
27332747

27342748
let res = bitcoin_controller.submit_operation(target_epoch_id, op, &mut op_signer, attempt);
27352749
match res {
2736-
Ok(_) => self.failed_to_submit_last_attempt = false,
2750+
Ok(_) => {
2751+
self.failed_to_submit_last_attempt = false;
2752+
self.globals
2753+
.counters
2754+
.bump_neon_submitted_commits(self.burn_block.block_height);
2755+
}
27372756
Err(_) if mock_mining => {
27382757
debug!("Relayer: Mock-mining enabled; not sending Bitcoin transaction");
27392758
self.failed_to_submit_last_attempt = true;
@@ -2858,6 +2877,7 @@ impl RelayerThread {
28582877

28592878
miner_thread: None,
28602879
mined_stacks_block: false,
2880+
last_attempt_failed: false,
28612881
}
28622882
}
28632883

@@ -3938,6 +3958,7 @@ impl RelayerThread {
39383958
let last_mined_block_opt = thread_handle
39393959
.join()
39403960
.expect("FATAL: failed to join miner thread");
3961+
self.last_attempt_failed = false;
39413962
if let Some(miner_result) = last_mined_block_opt {
39423963
match miner_result {
39433964
MinerThreadResult::Block(
@@ -4066,6 +4087,7 @@ impl RelayerThread {
40664087
}
40674088
}
40684089
} else {
4090+
self.last_attempt_failed = true;
40694091
// if we tried and failed to make an anchored block (e.g. because there's nothing to
40704092
// do), then resume microblock mining
40714093
if !self.mined_stacks_block {

testnet/stacks-node/src/run_loop/neon.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ pub struct Counters {
122122
pub sortitions_processed: RunLoopCounter,
123123

124124
pub naka_submitted_vrfs: RunLoopCounter,
125+
/// the number of submitted commits
126+
pub neon_submitted_commits: RunLoopCounter,
127+
/// the burn block height when the last commit was submitted
128+
pub neon_submitted_commit_last_burn_height: RunLoopCounter,
125129
pub naka_submitted_commits: RunLoopCounter,
126130
/// the burn block height when the last commit was submitted
127131
pub naka_submitted_commit_last_burn_height: RunLoopCounter,
@@ -196,6 +200,14 @@ impl Counters {
196200
Counters::inc(&self.cancelled_commits);
197201
}
198202

203+
pub fn bump_neon_submitted_commits(&self, committed_burn_height: u64) {
204+
Counters::inc(&self.neon_submitted_commits);
205+
Counters::set(
206+
&self.neon_submitted_commit_last_burn_height,
207+
committed_burn_height,
208+
);
209+
}
210+
199211
pub fn bump_naka_submitted_vrfs(&self) {
200212
Counters::inc(&self.naka_submitted_vrfs);
201213
}

0 commit comments

Comments
 (0)