Skip to content

Commit b8622ae

Browse files
committed
chore: do not tenure-extend if the last valid sortition is higher than the miner's tenure election. Instead, just stop mining, and hope that the winner of that last-valid sortition comes online. This commit adds the needful code to determine the last-valid sortition, and updates tenure_extend_after_failed_miner to expect a crash. It alters can_continue_tenure() to use this new inference.
1 parent 8e9303a commit b8622ae

File tree

6 files changed

+193
-75
lines changed

6 files changed

+193
-75
lines changed

stackslib/src/config/mod.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const DEFAULT_FIRST_REJECTION_PAUSE_MS: u64 = 5_000;
9494
const DEFAULT_SUBSEQUENT_REJECTION_PAUSE_MS: u64 = 10_000;
9595
const DEFAULT_BLOCK_COMMIT_DELAY_MS: u64 = 20_000;
9696
const DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE: u8 = 25;
97-
const DEFAULT_TENURE_EXTEND_WAIT_SECS: u64 = 30;
97+
const DEFAULT_TENURE_EXTEND_POLL_SECS: u64 = 1;
9898

9999
// This should be greater than the signers' timeout. This is used for issuing fallback tenure extends
100100
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 420;
@@ -2151,8 +2151,9 @@ pub struct MinerConfig {
21512151
pub block_commit_delay: Duration,
21522152
/// The percentage of the remaining tenure cost limit to consume each block.
21532153
pub tenure_cost_limit_per_block_percentage: Option<u8>,
2154-
/// The number of seconds to wait to try to continue a tenure if a BlockFound is expected
2155-
pub tenure_extend_wait_secs: Duration,
2154+
/// The number of seconds to wait in-between polling the sortition DB to see if we need to
2155+
/// extend the ongoing tenure (e.g. because the current sortition is empty or invalid).
2156+
pub tenure_extend_poll_secs: Duration,
21562157
/// Duration to wait before attempting to issue a tenure extend
21572158
pub tenure_timeout: Duration,
21582159
}
@@ -2191,7 +2192,7 @@ impl Default for MinerConfig {
21912192
tenure_cost_limit_per_block_percentage: Some(
21922193
DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE,
21932194
),
2194-
tenure_extend_wait_secs: Duration::from_secs(DEFAULT_TENURE_EXTEND_WAIT_SECS),
2195+
tenure_extend_poll_secs: Duration::from_secs(DEFAULT_TENURE_EXTEND_POLL_SECS),
21952196
tenure_timeout: Duration::from_secs(DEFAULT_TENURE_TIMEOUT_SECS),
21962197
}
21972198
}
@@ -2587,7 +2588,7 @@ pub struct MinerConfigFile {
25872588
pub subsequent_rejection_pause_ms: Option<u64>,
25882589
pub block_commit_delay_ms: Option<u64>,
25892590
pub tenure_cost_limit_per_block_percentage: Option<u8>,
2590-
pub tenure_extend_wait_secs: Option<u64>,
2591+
pub tenure_extend_poll_secs: Option<u64>,
25912592
pub tenure_timeout_secs: Option<u64>,
25922593
}
25932594

@@ -2729,7 +2730,7 @@ impl MinerConfigFile {
27292730
subsequent_rejection_pause_ms: self.subsequent_rejection_pause_ms.unwrap_or(miner_default_config.subsequent_rejection_pause_ms),
27302731
block_commit_delay: self.block_commit_delay_ms.map(Duration::from_millis).unwrap_or(miner_default_config.block_commit_delay),
27312732
tenure_cost_limit_per_block_percentage,
2732-
tenure_extend_wait_secs: self.tenure_extend_wait_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_extend_wait_secs),
2733+
tenure_extend_poll_secs: self.tenure_extend_poll_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_extend_poll_secs),
27332734
tenure_timeout: self.tenure_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_timeout),
27342735
})
27352736
}

testnet/stacks-node/src/nakamoto_node/miner.rs

Lines changed: 3 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use libsigner::v0::messages::{MinerSlotID, SignerMessage};
2525
use libsigner::StackerDBSession;
2626
use rand::{thread_rng, Rng};
2727
use stacks::burnchains::Burnchain;
28-
use stacks::chainstate::burn::db::sortdb::{get_ancestor_sort_id, SortitionDB};
28+
use stacks::chainstate::burn::db::sortdb::SortitionDB;
2929
use stacks::chainstate::burn::{BlockSnapshot, ConsensusHash};
3030
use stacks::chainstate::coordinator::OnChainRewardSetProvider;
3131
use stacks::chainstate::nakamoto::coordinator::load_nakamoto_reward_set;
@@ -1356,48 +1356,6 @@ impl BlockMinerThread {
13561356
Ok(ongoing_tenure_id)
13571357
}
13581358

1359-
/// Check to see if the given burn view is at or ahead of the stacks blockchain's burn view.
1360-
/// If so, then return Ok(())
1361-
/// If not, then return Err(NakamotoNodeError::BurnchainTipChanged)
1362-
pub fn check_burn_view_changed(
1363-
sortdb: &SortitionDB,
1364-
chain_state: &mut StacksChainState,
1365-
burn_view: &BlockSnapshot,
1366-
) -> Result<(), NakamotoNodeError> {
1367-
// if the local burn view has advanced, then this miner thread is defunct. Someone else
1368-
// extended their tenure in a sortition at or after our burn view, and the node accepted
1369-
// it, so we should stop.
1370-
let ongoing_tenure_id = Self::get_ongoing_tenure_id(sortdb, chain_state)?;
1371-
if ongoing_tenure_id.burn_view_consensus_hash != burn_view.consensus_hash {
1372-
let ongoing_tenure_sortition = SortitionDB::get_block_snapshot_consensus(
1373-
sortdb.conn(),
1374-
&ongoing_tenure_id.burn_view_consensus_hash,
1375-
)?
1376-
.ok_or_else(|| NakamotoNodeError::UnexpectedChainState)?;
1377-
1378-
// it's possible that our burn view is higher than the ongoing tenure's burn view, but
1379-
// if this *isn't* the case, then the Stacks burn view has necessarily advanced
1380-
let burn_view_tenure_handle = sortdb.index_handle_at_ch(&burn_view.consensus_hash)?;
1381-
if get_ancestor_sort_id(
1382-
&burn_view_tenure_handle,
1383-
ongoing_tenure_sortition.block_height,
1384-
&burn_view_tenure_handle.context.chain_tip,
1385-
)?
1386-
.is_none()
1387-
{
1388-
// ongoing tenure is not an ancestor of the given burn view, so it must have
1389-
// advanced (or forked) relative to the given burn view. Either way, this burn
1390-
// view has changed.
1391-
info!("Nakamoto chainstate burn view has advanced from miner burn view";
1392-
"nakamoto_burn_view" => %ongoing_tenure_id.burn_view_consensus_hash,
1393-
"miner_burn_view" => %burn_view.consensus_hash);
1394-
1395-
return Err(NakamotoNodeError::BurnchainTipChanged);
1396-
}
1397-
}
1398-
Ok(())
1399-
}
1400-
14011359
/// Check if the tenure needs to change -- if so, return a BurnchainTipChanged error
14021360
/// The tenure should change if there is a new burnchain tip with a valid sortition,
14031361
/// or if the stacks chain state's burn view has advanced beyond our burn view.
@@ -1406,8 +1364,6 @@ impl BlockMinerThread {
14061364
sortdb: &SortitionDB,
14071365
_chain_state: &mut StacksChainState,
14081366
) -> Result<(), NakamotoNodeError> {
1409-
// BlockMinerThread::check_burn_view_changed(sortdb, chain_state, &self.burn_block)?;
1410-
14111367
if let MinerReason::BlockFound { late } = &self.reason {
14121368
if *late && self.last_block_mined.is_none() {
14131369
// this is a late BlockFound tenure change that ought to be appended to the Stacks
@@ -1550,6 +1506,8 @@ impl ParentStacksBlockInfo {
15501506
"stacks_tip_burn_hash" => %parent_snapshot.burn_header_hash,
15511507
"stacks_tip_burn_height" => parent_snapshot.block_height,
15521508
"parent_tenure_info" => ?parent_tenure_info,
1509+
"stacks_tip_header.consensus_hash" => %stacks_tip_header.consensus_hash,
1510+
"parent_tenure_header.consensus_hash" => %parent_tenure_header.consensus_hash,
15531511
"reason" => %reason
15541512
);
15551513

testnet/stacks-node/src/nakamoto_node/relayer.rs

Lines changed: 161 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,14 @@ impl RelayerThread {
456456
});
457457
}
458458

459+
let mining_pkh_opt = self.get_mining_key_pkh();
460+
459461
// a sortition happened, but we didn't win.
460462
match Self::can_continue_tenure(
461463
&self.sortdb,
464+
&mut self.chainstate,
462465
sn.consensus_hash,
463-
self.get_mining_key_pkh(),
466+
mining_pkh_opt,
464467
) {
465468
Ok(Some(_)) => {
466469
// we can continue our ongoing tenure, but we should give the new winning miner
@@ -1124,21 +1127,147 @@ impl RelayerThread {
11241127
Ok(ih.get_last_snapshot_with_sortition(sort_tip.block_height)?)
11251128
}
11261129

1130+
/// Is the given sortition a valid sortition?
1131+
/// I.e. whose winning commit's parent tenure ID is on the canonical Stacks history,
1132+
/// and whose consensus hash corresponds to the ongoing tenure or a confirmed tenure?
1133+
fn is_valid_sortition(
1134+
chain_state: &mut StacksChainState,
1135+
stacks_tip_id: &StacksBlockId,
1136+
stacks_tip_sn: &BlockSnapshot,
1137+
burn_tip_ch: &ConsensusHash,
1138+
sn: &BlockSnapshot,
1139+
) -> Result<bool, NakamotoNodeError> {
1140+
if !sn.sortition {
1141+
// definitely not a valid sortition
1142+
debug!("Relayer: Sortition {} is empty", &sn.consensus_hash);
1143+
return Ok(false);
1144+
}
1145+
1146+
// check that this commit's parent tenure ID is on the history tipped at
1147+
// `stacks_tip_id`
1148+
let mut ic = chain_state.index_conn();
1149+
let parent_tenure_id = StacksBlockId(sn.winning_stacks_block_hash.clone().0);
1150+
let height_opt = ic.get_ancestor_block_height(&parent_tenure_id, stacks_tip_id)?;
1151+
if height_opt.is_none() {
1152+
// parent_tenure_id is not an ancestor of stacks_tip_id
1153+
debug!(
1154+
"Relayer: Sortition {} has winning commit hash {}, which is not canonical",
1155+
&sn.consensus_hash, &parent_tenure_id
1156+
);
1157+
return Ok(false);
1158+
}
1159+
1160+
if sn.consensus_hash == *burn_tip_ch {
1161+
// sn is the sortition tip, so this sortition must commit to the tenure start block of
1162+
// the ongoing Stacks tenure.
1163+
let highest_tenure_start_block_header = NakamotoChainState::get_tenure_start_block_header(
1164+
&mut ic,
1165+
stacks_tip_id,
1166+
&stacks_tip_sn.consensus_hash
1167+
)?
1168+
.ok_or_else(|| {
1169+
error!(
1170+
"Relayer: Failed to find tenure-start block header for stacks tip {stacks_tip_id}"
1171+
);
1172+
NakamotoNodeError::ParentNotFound
1173+
})?;
1174+
1175+
let highest_tenure_start_block_id =
1176+
highest_tenure_start_block_header.index_block_hash();
1177+
if highest_tenure_start_block_id != parent_tenure_id {
1178+
debug!("Relayer: Sortition {} is at the tip, but does not commit to {} so cannot be valid", &sn.consensus_hash, &parent_tenure_id;
1179+
"highest_tenure_start_block_header.block_id()" => %highest_tenure_start_block_id);
1180+
return Ok(false);
1181+
}
1182+
}
1183+
1184+
Ok(true)
1185+
}
1186+
1187+
/// Determine the highest valid sortition higher than `elected_tenure_id`, but no higher than
1188+
/// `sort_tip`.
1189+
///
1190+
/// This is the highest non-empty sortition (up to and including `sort_tip`)
1191+
/// whose winning commit's parent tenure ID matches the
1192+
/// Stacks tip, and whose consensus hash matches the Stacks tip's tenure ID.
1193+
///
1194+
/// Returns Ok(Some(..)) if such a sortition is found, and is higher than that of
1195+
/// `elected_tenure_id`.
1196+
/// Returns Ok(None) if no such sortition is found.
1197+
/// Returns Err(..) on DB errors.
1198+
fn find_highest_valid_sortition(
1199+
sortdb: &SortitionDB,
1200+
chain_state: &mut StacksChainState,
1201+
sort_tip: &BlockSnapshot,
1202+
elected_tenure_id: &ConsensusHash,
1203+
) -> Result<Option<BlockSnapshot>, NakamotoNodeError> {
1204+
// sanity check -- if sort_tip is the elected_tenure_id sortition, then there are no higher
1205+
// valid sortitions.
1206+
if sort_tip.consensus_hash == *elected_tenure_id {
1207+
return Ok(None);
1208+
}
1209+
1210+
let mut cursor = sort_tip.clone();
1211+
let (canonical_stacks_tip_ch, canonical_stacks_tip_bh) =
1212+
SortitionDB::get_canonical_stacks_chain_tip_hash(sortdb.conn()).unwrap();
1213+
let canonical_stacks_tip =
1214+
StacksBlockId::new(&canonical_stacks_tip_ch, &canonical_stacks_tip_bh);
1215+
1216+
let Ok(Some(canonical_stacks_tip_sn)) =
1217+
SortitionDB::get_block_snapshot_consensus(sortdb.conn(), &canonical_stacks_tip_ch)
1218+
else {
1219+
return Err(NakamotoNodeError::ParentNotFound);
1220+
};
1221+
1222+
loop {
1223+
debug!(
1224+
"Relayer: check sortition {} to see if it is valid",
1225+
&cursor.consensus_hash
1226+
);
1227+
1228+
// is this a valid sortiton?
1229+
if Self::is_valid_sortition(
1230+
chain_state,
1231+
&canonical_stacks_tip,
1232+
&canonical_stacks_tip_sn,
1233+
&sort_tip.consensus_hash,
1234+
&cursor,
1235+
)? {
1236+
return Ok(Some(cursor));
1237+
}
1238+
1239+
// nope. continue the search
1240+
let Some(cursor_parent) =
1241+
SortitionDB::get_block_snapshot(sortdb.conn(), &cursor.parent_sortition_id)?
1242+
else {
1243+
return Ok(None);
1244+
};
1245+
1246+
if cursor_parent.consensus_hash == *elected_tenure_id {
1247+
return Ok(None);
1248+
}
1249+
1250+
cursor = cursor_parent;
1251+
}
1252+
}
1253+
11271254
/// Determine if the miner can contine an existing tenure with the new sortition (identified
11281255
/// by `new_burn_view`)
11291256
///
11301257
/// Assumes that the caller has already checked that the given miner has _not_ won the new
11311258
/// sortition.
11321259
///
1133-
/// Will return Ok(Some(..)) even if `new_burn_view`'s sortition had a winner that was not this
1134-
/// miner. It's on signers to either accept the resulting tenure-extend from this miner, or a
1135-
/// block-found from the other winning miner.
1260+
/// Returns Ok(Some(stacks-tip-election-snapshot)) if the last-winning miner needs to extend.
1261+
/// For now, this only happens if the miner's election snapshot was the last-known valid and
1262+
/// non-empty snapshot. In the future, this function may return Ok(Some(..)) if the node
1263+
/// determines that a subsequent miner won sortition, but never came online.
1264+
///
1265+
/// Returns OK(None) if the last-winning miner should not extend its tenure.
11361266
///
1137-
/// Returns Ok(Some(stacks-tip-election-snapshot)) if so
1138-
/// Returns OK(None) if not.
11391267
/// Returns Err(..) on DB error
11401268
pub(crate) fn can_continue_tenure(
11411269
sortdb: &SortitionDB,
1270+
chain_state: &mut StacksChainState,
11421271
new_burn_view: ConsensusHash,
11431272
mining_key_opt: Option<Hash160>,
11441273
) -> Result<Option<BlockSnapshot>, NakamotoNodeError> {
@@ -1187,6 +1316,22 @@ impl RelayerThread {
11871316
return Ok(None);
11881317
}
11891318

1319+
// For now, only allow the miner to extend its tenure if won the highest valid sortition.
1320+
// There cannot be any higher sortitions that are valid (as defined above).
1321+
//
1322+
// In the future, the miner will be able to extend its tenure even if there are higher
1323+
// valid sortitions, but only if it determines that the miners of those sortitions are
1324+
// offline.
1325+
if let Some(highest_valid_sortition) = Self::find_highest_valid_sortition(
1326+
sortdb,
1327+
chain_state,
1328+
&sort_tip,
1329+
&canonical_stacks_snapshot.consensus_hash,
1330+
)? {
1331+
info!("Relayer: will not extend tenure -- we won sortition {}, but the highest valid sortition is {}", &canonical_stacks_snapshot.consensus_hash, &highest_valid_sortition.consensus_hash);
1332+
return Ok(None);
1333+
}
1334+
11901335
Ok(Some(canonical_stacks_snapshot))
11911336
}
11921337

@@ -1203,10 +1348,12 @@ impl RelayerThread {
12031348
}
12041349
debug!("Relayer: successfully stopped tenure; will try to continue.");
12051350

1351+
let mining_pkh_opt = self.get_mining_key_pkh();
12061352
let Some(canonical_stacks_tip_election_snapshot) = Self::can_continue_tenure(
12071353
&self.sortdb,
1354+
&mut self.chainstate,
12081355
new_burn_view.clone(),
1209-
self.get_mining_key_pkh(),
1356+
mining_pkh_opt,
12101357
)?
12111358
else {
12121359
return Ok(());
@@ -1514,24 +1661,26 @@ impl RelayerThread {
15141661
))
15151662
}
15161663

1517-
/// Try to start up a tenure-extend, after a delay has passed.
1518-
/// We would do this if we were the miner of the ongoing tenure, but did not win the last
1519-
/// sortition, and the winning miner never produced a block.
1664+
/// Try to start up a tenure-extend.
1665+
/// Only do this if the miner won the last-ever sortition but the burn view has changed.
1666+
/// In the future, the miner will also try to extend its tenure if a subsequent miner appears
1667+
/// to be offline.
15201668
fn try_continue_tenure(&mut self) {
15211669
if self.tenure_extend_timeout.is_none() {
15221670
return;
15231671
}
15241672

1673+
// time to poll to see if we should begin a tenure-extend?
15251674
let deadline_passed = self
15261675
.tenure_extend_timeout
15271676
.map(|tenure_extend_timeout| {
15281677
let deadline_passed =
1529-
tenure_extend_timeout.elapsed() > self.config.miner.tenure_extend_wait_secs;
1678+
tenure_extend_timeout.elapsed() > self.config.miner.tenure_extend_poll_secs;
15301679
if !deadline_passed {
15311680
test_debug!(
15321681
"Relayer: will not try to tenure-extend yet ({} <= {})",
15331682
tenure_extend_timeout.elapsed().as_secs(),
1534-
self.config.miner.tenure_extend_wait_secs.as_secs()
1683+
self.config.miner.tenure_extend_poll_secs.as_secs()
15351684
);
15361685
}
15371686
deadline_passed

testnet/stacks-node/src/nakamoto_node/signer_coordinator.rs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,6 @@ impl SignerCoordinator {
371371
_chain_state: &mut StacksChainState,
372372
burn_block: &BlockSnapshot,
373373
) -> bool {
374-
/*
375-
if BlockMinerThread::check_burn_view_changed(sortdb, chain_state, burn_block).is_err() {
376-
// can't continue mining -- burn view changed, or a DB error occurred
377-
return true;
378-
}
379-
*/
380-
381374
let cur_burn_chain_tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
382375
.expect("FATAL: failed to query sortition DB for canonical burn chain tip");
383376

0 commit comments

Comments
 (0)