Skip to content

Commit 0fd41fb

Browse files
committed
Merge branch 'develop' of https://github.com/stacks-network/stacks-core into bug/validation-queue-race-condition
2 parents 8489211 + d13bdb8 commit 0fd41fb

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+6536
-878
lines changed

.github/actions/dockerfiles/Dockerfile.debian-source

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ RUN --mount=type=tmpfs,target=${BUILD_DIR} cp -R /src/. ${BUILD_DIR}/ \
2424
&& cp -R ${BUILD_DIR}/target/${TARGET}/release/. /out
2525

2626
FROM --platform=${TARGETPLATFORM} debian:bookworm
27-
COPY --from=build /out/stacks-node /out/stacks-signer /bin/
27+
COPY --from=build /out/stacks-node /out/stacks-signer /out/stacks-inspect /bin/
2828
CMD ["stacks-node", "mainnet"]

.github/workflows/bitcoin-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ jobs:
126126
- tests::signer::v0::block_commit_delay
127127
- tests::signer::v0::continue_after_fast_block_no_sortition
128128
- tests::signer::v0::block_validation_response_timeout
129+
- tests::signer::v0::tenure_extend_after_bad_commit
129130
- tests::nakamoto_integrations::burn_ops_integration_test
130131
- tests::nakamoto_integrations::check_block_heights
131132
- tests::nakamoto_integrations::clarity_burn_state
@@ -139,6 +140,7 @@ jobs:
139140
- tests::nakamoto_integrations::utxo_check_on_startup_panic
140141
- tests::nakamoto_integrations::utxo_check_on_startup_recover
141142
- tests::nakamoto_integrations::v3_signer_api_endpoint
143+
- tests::nakamoto_integrations::test_shadow_recovery
142144
- tests::nakamoto_integrations::signer_chainstate
143145
- tests::nakamoto_integrations::clarity_cost_spend_down
144146
- tests::nakamoto_integrations::v3_blockbyheight_api_endpoint

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1414
- Add `block_commit_delay_ms` to the config file to control the time to wait after seeing a new burn block, before submitting a block commit, to allow time for the first Nakamoto block of the new tenure to be mined, allowing this miner to avoid the need to RBF the block commit.
1515
- Add `tenure_cost_limit_per_block_percentage` to the miner config file to control the percentage remaining tenure cost limit to consume per nakamoto block.
1616
- Add `/v3/blocks/height/:block_height` rpc endpoint
17+
- If the winning miner of a sortition is committed to the wrong parent tenure, the previous miner can immediately tenure extend and continue mining since the winning miner would never be able to propose a valid block. (#5361)
1718

1819
## [3.0.0.0.1]
1920

stacks-common/src/types/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ impl StacksEpochId {
170170
}
171171
}
172172

173+
/// Whether or not this epoch supports shadow blocks
174+
pub fn supports_shadow_blocks(&self) -> bool {
175+
match self {
176+
StacksEpochId::Epoch10
177+
| StacksEpochId::Epoch20
178+
| StacksEpochId::Epoch2_05
179+
| StacksEpochId::Epoch21
180+
| StacksEpochId::Epoch22
181+
| StacksEpochId::Epoch23
182+
| StacksEpochId::Epoch24
183+
| StacksEpochId::Epoch25 => false,
184+
StacksEpochId::Epoch30 => true,
185+
}
186+
}
187+
173188
/// Does this epoch support unlocking PoX contributors that miss a slot?
174189
///
175190
/// Epoch 2.0 - 2.05 didn't support this feature, but they weren't epoch-guarded on it. Instead,

stacks-signer/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1111

1212
### Changed
1313

14+
- Allow a miner to extend their tenure immediately if the winner of the next tenure has committed to the wrong parent tenure (#5361)
15+
1416
## [3.0.0.0.1.0]
1517

1618
### Changed

stacks-signer/src/chainstate.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,40 @@ impl SortitionsView {
203203
"current_sortition_consensus_hash" => ?self.cur_sortition.consensus_hash,
204204
);
205205
self.cur_sortition.miner_status = SortitionMinerStatus::InvalidatedBeforeFirstBlock;
206+
} else if let Some(tip) = signer_db.get_canonical_tip()? {
207+
// Check if the current sortition is aligned with the expected tenure:
208+
// - If the tip is in the current tenure, we are in the process of mining this tenure.
209+
// - If the tip is not in the current tenure, then we’re starting a new tenure,
210+
// and the current sortition's parent tenure must match the tenure of the tip.
211+
// - If the tip is not building off of the current sortition's parent tenure, then
212+
// check to see if the tip's parent is within the first proposal burn block timeout,
213+
// which allows for forks when a burn block arrives quickly.
214+
// - Else the miner of the current sortition has committed to an incorrect parent tenure.
215+
let consensus_hash_match =
216+
self.cur_sortition.consensus_hash == tip.block.header.consensus_hash;
217+
let parent_tenure_id_match =
218+
self.cur_sortition.parent_tenure_id == tip.block.header.consensus_hash;
219+
if !consensus_hash_match && !parent_tenure_id_match {
220+
// More expensive check, so do it only if we need to.
221+
let is_valid_parent_tenure = Self::check_parent_tenure_choice(
222+
&self.cur_sortition,
223+
block,
224+
signer_db,
225+
client,
226+
&self.config.first_proposal_burn_block_timing,
227+
)?;
228+
if !is_valid_parent_tenure {
229+
warn!(
230+
"Current sortition does not build off of canonical tip tenure, marking as invalid";
231+
"current_sortition_parent" => ?self.cur_sortition.parent_tenure_id,
232+
"tip_consensus_hash" => ?tip.block.header.consensus_hash,
233+
);
234+
self.cur_sortition.miner_status =
235+
SortitionMinerStatus::InvalidatedBeforeFirstBlock;
236+
}
237+
}
206238
}
239+
207240
if let Some(last_sortition) = self.last_sortition.as_mut() {
208241
if last_sortition.is_timed_out(self.config.block_proposal_timeout, signer_db)? {
209242
info!(
@@ -304,6 +337,7 @@ impl SortitionsView {
304337
"Miner block proposal is from last sortition winner, when the new sortition winner is still valid. Considering proposal invalid.";
305338
"proposed_block_consensus_hash" => %block.header.consensus_hash,
306339
"proposed_block_signer_sighash" => %block.header.signer_signature_hash(),
340+
"current_sortition_miner_status" => ?self.cur_sortition.miner_status,
307341
);
308342
return Ok(false);
309343
}
@@ -439,6 +473,8 @@ impl SortitionsView {
439473
"violating_tenure_proposed_time" => local_block_info.proposed_time,
440474
"new_tenure_received_time" => sortition_state_received_time,
441475
"new_tenure_burn_timestamp" => sortition_state.burn_header_timestamp,
476+
"first_proposal_burn_block_timing_secs" => first_proposal_burn_block_timing.as_secs(),
477+
"proposal_to_sortition" => proposal_to_sortition,
442478
);
443479
continue;
444480
}

stacks-signer/src/signerdb.rs

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,11 @@ static CREATE_INDEXES_3: &str = r#"
328328
CREATE INDEX IF NOT EXISTS block_rejection_signer_addrs_on_block_signature_hash ON block_rejection_signer_addrs(signer_signature_hash);
329329
"#;
330330

331+
static CREATE_INDEXES_4: &str = r#"
332+
CREATE INDEX IF NOT EXISTS blocks_state ON blocks ((json_extract(block_info, '$.state')));
333+
CREATE INDEX IF NOT EXISTS blocks_signed_group ON blocks ((json_extract(block_info, '$.signed_group')));
334+
"#;
335+
331336
static CREATE_SIGNER_STATE_TABLE: &str = "
332337
CREATE TABLE IF NOT EXISTS signer_states (
333338
reward_cycle INTEGER PRIMARY KEY,
@@ -425,9 +430,14 @@ static SCHEMA_3: &[&str] = &[
425430
"INSERT INTO db_config (version) VALUES (3);",
426431
];
427432

433+
static SCHEMA_4: &[&str] = &[
434+
CREATE_INDEXES_4,
435+
"INSERT OR REPLACE INTO db_config (version) VALUES (4);",
436+
];
437+
428438
impl SignerDb {
429439
/// The current schema version used in this build of the signer binary.
430-
pub const SCHEMA_VERSION: u32 = 3;
440+
pub const SCHEMA_VERSION: u32 = 4;
431441

432442
/// Create a new `SignerState` instance.
433443
/// This will create a new SQLite database at the given path
@@ -447,7 +457,7 @@ impl SignerDb {
447457
return Ok(0);
448458
}
449459
let result = conn
450-
.query_row("SELECT version FROM db_config LIMIT 1", [], |row| {
460+
.query_row("SELECT MAX(version) FROM db_config LIMIT 1", [], |row| {
451461
row.get(0)
452462
})
453463
.optional();
@@ -499,6 +509,20 @@ impl SignerDb {
499509
Ok(())
500510
}
501511

512+
/// Migrate from schema 3 to schema 4
513+
fn schema_4_migration(tx: &Transaction) -> Result<(), DBError> {
514+
if Self::get_schema_version(tx)? >= 4 {
515+
// no migration necessary
516+
return Ok(());
517+
}
518+
519+
for statement in SCHEMA_4.iter() {
520+
tx.execute_batch(statement)?;
521+
}
522+
523+
Ok(())
524+
}
525+
502526
/// Either instantiate a new database, or migrate an existing one
503527
/// If the detected version of the existing database is 0 (i.e., a pre-migration
504528
/// logic DB, the DB will be dropped).
@@ -510,7 +534,8 @@ impl SignerDb {
510534
0 => Self::schema_1_migration(&sql_tx)?,
511535
1 => Self::schema_2_migration(&sql_tx)?,
512536
2 => Self::schema_3_migration(&sql_tx)?,
513-
3 => break,
537+
3 => Self::schema_4_migration(&sql_tx)?,
538+
4 => break,
514539
x => return Err(DBError::Other(format!(
515540
"Database schema is newer than supported by this binary. Expected version = {}, Database version = {x}",
516541
Self::SCHEMA_VERSION,
@@ -620,6 +645,15 @@ impl SignerDb {
620645
try_deserialize(result)
621646
}
622647

648+
/// Return the canonical tip -- the last globally accepted block.
649+
pub fn get_canonical_tip(&self) -> Result<Option<BlockInfo>, DBError> {
650+
let query = "SELECT block_info FROM blocks WHERE json_extract(block_info, '$.state') = ?1 ORDER BY stacks_height DESC, json_extract(block_info, '$.signed_group') DESC LIMIT 1";
651+
let args = params![&BlockState::GloballyAccepted.to_string()];
652+
let result: Option<String> = query_row(&self.db, query, args)?;
653+
654+
try_deserialize(result)
655+
}
656+
623657
/// Insert or replace a burn block into the database
624658
pub fn insert_burn_block(
625659
&mut self,
@@ -1263,4 +1297,45 @@ mod tests {
12631297
assert!(!block.check_state(BlockState::GloballyAccepted));
12641298
assert!(block.check_state(BlockState::GloballyRejected));
12651299
}
1300+
1301+
#[test]
1302+
fn test_get_canonical_tip() {
1303+
let db_path = tmp_db_path();
1304+
let mut db = SignerDb::new(db_path).expect("Failed to create signer db");
1305+
1306+
let (mut block_info_1, _block_proposal_1) = create_block_override(|b| {
1307+
b.block.header.miner_signature = MessageSignature([0x01; 65]);
1308+
b.block.header.chain_length = 1;
1309+
b.burn_height = 1;
1310+
});
1311+
1312+
let (mut block_info_2, _block_proposal_2) = create_block_override(|b| {
1313+
b.block.header.miner_signature = MessageSignature([0x02; 65]);
1314+
b.block.header.chain_length = 2;
1315+
b.burn_height = 2;
1316+
});
1317+
1318+
db.insert_block(&block_info_1)
1319+
.expect("Unable to insert block into db");
1320+
db.insert_block(&block_info_2)
1321+
.expect("Unable to insert block into db");
1322+
1323+
assert!(db.get_canonical_tip().unwrap().is_none());
1324+
1325+
block_info_1
1326+
.mark_globally_accepted()
1327+
.expect("Failed to mark block as globally accepted");
1328+
db.insert_block(&block_info_1)
1329+
.expect("Unable to insert block into db");
1330+
1331+
assert_eq!(db.get_canonical_tip().unwrap().unwrap(), block_info_1);
1332+
1333+
block_info_2
1334+
.mark_globally_accepted()
1335+
.expect("Failed to mark block as globally accepted");
1336+
db.insert_block(&block_info_2)
1337+
.expect("Unable to insert block into db");
1338+
1339+
assert_eq!(db.get_canonical_tip().unwrap().unwrap(), block_info_2);
1340+
}
12661341
}

stackslib/src/burnchains/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ impl PoxConstants {
450450
)
451451
}
452452

453+
// NOTE: this is the *old* pre-Nakamoto testnet
453454
pub fn testnet_default() -> PoxConstants {
454455
PoxConstants::new(
455456
POX_REWARD_CYCLE_LENGTH / 2, // 1050
@@ -468,6 +469,10 @@ impl PoxConstants {
468469
) // total liquid supply is 40000000000000000 µSTX
469470
}
470471

472+
pub fn nakamoto_testnet_default() -> PoxConstants {
473+
PoxConstants::new(900, 100, 51, 100, 0, u64::MAX, u64::MAX, 242, 243, 246, 244)
474+
}
475+
471476
// TODO: add tests from mutation testing results #4838
472477
#[cfg_attr(test, mutants::skip)]
473478
pub fn regtest_default() -> PoxConstants {

stackslib/src/burnchains/tests/mod.rs

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,30 @@ impl TestMinerFactory {
351351

352352
impl TestBurnchainBlock {
353353
pub fn new(parent_snapshot: &BlockSnapshot, fork_id: u64) -> TestBurnchainBlock {
354+
let burn_header_hash = BurnchainHeaderHash::from_test_data(
355+
parent_snapshot.block_height + 1,
356+
&parent_snapshot.index_root,
357+
fork_id,
358+
);
354359
TestBurnchainBlock {
355360
parent_snapshot: parent_snapshot.clone(),
356361
block_height: parent_snapshot.block_height + 1,
357-
txs: vec![],
362+
txs: vec![
363+
// make sure that no block-commit gets vtxindex == 0 unless explicitly structured.
364+
// This prestx mocks a burnchain coinbase
365+
BlockstackOperationType::PreStx(PreStxOp {
366+
output: StacksAddress::burn_address(false),
367+
txid: Txid::from_test_data(
368+
parent_snapshot.block_height + 1,
369+
0,
370+
&burn_header_hash,
371+
128,
372+
),
373+
vtxindex: 0,
374+
block_height: parent_snapshot.block_height + 1,
375+
burn_header_hash,
376+
}),
377+
],
358378
fork_id: fork_id,
359379
timestamp: get_epoch_time_secs(),
360380
}
@@ -397,6 +417,7 @@ impl TestBurnchainBlock {
397417
parent_block_snapshot: Option<&BlockSnapshot>,
398418
new_seed: Option<VRFSeed>,
399419
epoch_marker: u8,
420+
parent_is_shadow: bool,
400421
) -> LeaderBlockCommitOp {
401422
let pubks = miner
402423
.privks
@@ -435,6 +456,13 @@ impl TestBurnchainBlock {
435456
)
436457
.expect("FATAL: failed to read block commit");
437458

459+
if parent_is_shadow {
460+
assert!(
461+
get_commit_res.is_none(),
462+
"FATAL: shadow parent should not have a block-commit"
463+
);
464+
}
465+
438466
let input = SortitionDB::get_last_block_commit_by_sender(ic.conn(), &apparent_sender)
439467
.unwrap()
440468
.map(|commit| (commit.txid.clone(), 1 + (commit.commit_outs.len() as u32)))
@@ -454,7 +482,8 @@ impl TestBurnchainBlock {
454482
block_hash,
455483
self.block_height,
456484
&new_seed,
457-
&parent,
485+
parent.block_height as u32,
486+
parent.vtxindex as u16,
458487
leader_key.block_height as u32,
459488
leader_key.vtxindex as u16,
460489
burn_fee,
@@ -464,16 +493,42 @@ impl TestBurnchainBlock {
464493
txop
465494
}
466495
None => {
467-
// initial
468-
let txop = LeaderBlockCommitOp::initial(
469-
block_hash,
470-
self.block_height,
471-
&new_seed,
472-
leader_key,
473-
burn_fee,
474-
&input,
475-
&apparent_sender,
476-
);
496+
let txop = if parent_is_shadow {
497+
test_debug!(
498+
"Block-commit for {} (burn height {}) builds on shadow sortition",
499+
block_hash,
500+
self.block_height
501+
);
502+
503+
LeaderBlockCommitOp::new(
504+
block_hash,
505+
self.block_height,
506+
&new_seed,
507+
last_snapshot_with_sortition.block_height as u32,
508+
0,
509+
leader_key.block_height as u32,
510+
leader_key.vtxindex as u16,
511+
burn_fee,
512+
&input,
513+
&apparent_sender,
514+
)
515+
} else {
516+
// initial
517+
test_debug!(
518+
"Block-commit for {} (burn height {}) builds on genesis",
519+
block_hash,
520+
self.block_height,
521+
);
522+
LeaderBlockCommitOp::initial(
523+
block_hash,
524+
self.block_height,
525+
&new_seed,
526+
leader_key,
527+
burn_fee,
528+
&input,
529+
&apparent_sender,
530+
)
531+
};
477532
txop
478533
}
479534
};
@@ -517,6 +572,7 @@ impl TestBurnchainBlock {
517572
parent_block_snapshot,
518573
None,
519574
STACKS_EPOCH_2_4_MARKER,
575+
false,
520576
)
521577
}
522578

0 commit comments

Comments
 (0)