Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions libsigner/src/v0/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1072,6 +1072,7 @@ impl From<&RejectReason> for RejectReasonPrefix {
RejectReason::NoSignerConsensus => RejectReasonPrefix::NoSignerConsensus,
RejectReason::ConsensusHashMismatch { .. } => RejectReasonPrefix::ConsensusHashMismatch,
RejectReason::ProblematicTransactions => RejectReasonPrefix::ProblematicTransactions,
RejectReason::ProposalTooOld => RejectReasonPrefix::ProposalTooOld,
RejectReason::Unknown(_) => RejectReasonPrefix::Unknown,
RejectReason::NotRejected => RejectReasonPrefix::NotRejected,
}
Expand Down Expand Up @@ -1160,6 +1161,9 @@ pub enum RejectReason {
/// The block marks one or more transactions as problematic, which signers
/// do not yet allow
ProblematicTransactions,
/// The block proposal's header timestamp is older than the signer's
/// configured `block_proposal_max_age_secs`
ProposalTooOld,
/// The block was approved, no rejection details needed
NotRejected,
/// Handle unknown codes gracefully
Expand Down Expand Up @@ -1210,6 +1214,9 @@ pub enum RejectReasonPrefix {
/// The block marks one or more transactions as problematic, which signers
/// do not yet allow
ProblematicTransactions = 17,
/// The block proposal's header timestamp is older than the signer's
/// configured `block_proposal_max_age_secs`
ProposalTooOld = 18,
/// Unknown reject code, for forward compatibility
Unknown = 254,
/// The block was approved, no rejection details needed
Expand Down Expand Up @@ -1238,6 +1245,7 @@ impl RejectReasonPrefix {
Self::NoSignerConsensus => 15,
Self::ConsensusHashMismatch => 16,
Self::ProblematicTransactions => 17,
Self::ProposalTooOld => 18,
Self::Unknown => 254,
Self::NotRejected => 255,
}
Expand Down Expand Up @@ -1265,6 +1273,7 @@ impl From<u8> for RejectReasonPrefix {
15 => Self::NoSignerConsensus,
16 => Self::ConsensusHashMismatch,
17 => Self::ProblematicTransactions,
18 => Self::ProposalTooOld,
255 => Self::NotRejected,
// For forward compatibility, all other values are unknown
_ => Self::Unknown,
Expand Down Expand Up @@ -1930,6 +1939,7 @@ impl StacksMessageCodec for RejectReason {
| RejectReason::IrrecoverablePubkeyHash
| RejectReason::NoSignerConsensus
| RejectReason::ProblematicTransactions
| RejectReason::ProposalTooOld
| RejectReason::Unknown(_)
| RejectReason::NotRejected => {
// No additional data to serialize / deserialize
Expand Down Expand Up @@ -1975,6 +1985,7 @@ impl StacksMessageCodec for RejectReason {
RejectReason::ConsensusHashMismatch { expected, actual }
}
RejectReasonPrefix::ProblematicTransactions => RejectReason::ProblematicTransactions,
RejectReasonPrefix::ProposalTooOld => RejectReason::ProposalTooOld,
RejectReasonPrefix::Unknown => RejectReason::Unknown(type_prefix_byte),
RejectReasonPrefix::NotRejected => RejectReason::NotRejected,
};
Expand Down Expand Up @@ -2081,6 +2092,12 @@ impl std::fmt::Display for RejectReason {
"The block has an irrecoverable associated miner public key hash."
)
}
RejectReason::ProposalTooOld => {
write!(
f,
"The block proposal's header timestamp is older than the maximum proposal age."
)
}
RejectReason::NoSignerConsensus => {
write!(f, "No signer consensus reached.")
}
Expand Down
28 changes: 22 additions & 6 deletions stacks-node/src/tests/signer/v0/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub mod failed_txs;
pub mod late_block_proposal;
pub mod missing_burn_block_proposal;
pub mod problematic_txs;
pub mod proposal_replication_void;
pub mod reorg;
pub mod signers_consider_consensus_blocks;
pub mod signers_consider_late_proposals;
Expand Down Expand Up @@ -2529,6 +2530,7 @@ pub fn wait_for_block_rejections_from_signers(
})?;
Ok(result)
}

/// Waits for at least 70% of the provided signers to send an update for a block with the specificed burn block height and parent tenure stacks block height and message version
pub fn wait_for_state_machine_update(
timeout_secs: u64,
Expand Down Expand Up @@ -5979,8 +5981,10 @@ fn block_validation_pending_table() {

#[test]
#[ignore]
/// Test the block_proposal_max_age_secs signer configuration option. It should reject blocks that are
/// invalid but within the max age window, otherwise it should simply drop the block without further processing.
/// Test the block_proposal_max_age_secs signer configuration option. Blocks that are
/// invalid but within the max age window are rejected after validation; blocks past the
/// max age window are rejected immediately (without validation) with ProposalTooOld so
/// the miner learns to re-mine instead of re-sending the same stale block forever.
///
/// Test Setup:
/// The test spins up five stacks signers, one miner Nakamoto node, and a corresponding bitcoind.
Expand All @@ -5990,11 +5994,12 @@ fn block_validation_pending_table() {
/// An invalid block proposal with a recent timestamp is forcibly written to the miner's slot to simulate the miner proposing a block.
/// The signers process the invalid block and broadcast a block response rejection to the respective .signers-XXX-YYY contract.
/// A second block proposal with an outdated timestamp is then submitted to the miner's slot to simulate the miner proposing a very old block.
/// The test confirms no further block rejection response is submitted to the .signers-XXX-YYY contract.
/// The test confirms the stale proposal is also rejected (with ProposalTooOld), and never accepted.
///
/// Test Assertion:
/// - Each signer successfully rejects the recent invalid block proposal.
/// - No signer submits a block proposal response for the outdated block proposal.
/// - Each signer rejects the outdated block proposal with the ProposalTooOld reason.
/// - No signer accepts either block.
/// - The stacks tip does not advance
fn block_proposal_max_age_rejections() {
if env::var("BITCOIND_TEST") != Ok("1".into()) {
Expand Down Expand Up @@ -6054,8 +6059,16 @@ fn block_proposal_max_age_rejections() {
match message {
SignerMessage::BlockResponse(BlockResponse::Rejected(BlockRejection {
signer_signature_hash,
response_data,
..
})) => {
if signer_signature_hash == block_signer_signature_hash_1 {
assert_eq!(
response_data.reject_reason,
RejectReason::ProposalTooOld,
"Stale proposal must be rejected as ProposalTooOld"
);
}
let entry = status_map.entry(signer_signature_hash).or_insert((0, 0));
entry.0 += 1;
}
Expand All @@ -6073,7 +6086,10 @@ fn block_proposal_max_age_rejections() {
.get(&block_signer_signature_hash_1)
.cloned()
.unwrap_or((0, 0));
assert_eq!(block_1_status, (0, 0));
assert_eq!(
block_1_status.1, 0,
"Block 1 (stale) must never be accepted"
);

let block_2_status = status_map
.get(&block_signer_signature_hash_2)
Expand All @@ -6084,7 +6100,7 @@ fn block_proposal_max_age_rejections() {
info!("Block 2 status";
"accepted" => %block_2_status.1, "rejected" => %block_2_status.0
);
Ok(block_2_status.0 > num_signers * 7 / 10)
Ok(block_2_status.0 > num_signers * 7 / 10 && block_1_status.0 > num_signers * 7 / 10)
})
.expect("Timed out waiting for block rejections");

Expand Down
Loading