Skip to content

Commit 5b6eae4

Browse files
committed
CRC: move sortition time out check into its own function
Signed-off-by: Jacinta Ferrant <[email protected]>
1 parent cf25665 commit 5b6eae4

File tree

2 files changed

+118
-25
lines changed

2 files changed

+118
-25
lines changed

stacks-signer/src/chainstate.rs

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,39 @@ pub struct SortitionState {
7878
pub burn_block_hash: BurnchainHeaderHash,
7979
}
8080

81+
impl SortitionState {
82+
/// Check if the sortition is timed out (i.e., the miner did not propose a block in time)
83+
pub fn is_timed_out(
84+
&self,
85+
timeout: Duration,
86+
signer_db: &SignerDb,
87+
) -> Result<bool, SignerChainstateError> {
88+
// if the miner has already been invalidated, we don't need to check if they've timed out.
89+
if self.miner_status != SortitionMinerStatus::Valid {
90+
return Ok(false);
91+
}
92+
// if we've already signed a block in this tenure, the miner can't have timed out.
93+
let has_blocks = signer_db
94+
.get_last_signed_block_in_tenure(&self.consensus_hash)?
95+
.is_some();
96+
if has_blocks {
97+
return Ok(false);
98+
}
99+
let Some(received_ts) = signer_db.get_burn_block_receive_time(&self.burn_block_hash)?
100+
else {
101+
return Ok(false);
102+
};
103+
let received_time = UNIX_EPOCH + Duration::from_secs(received_ts);
104+
let Ok(elapsed) = std::time::SystemTime::now().duration_since(received_time) else {
105+
return Ok(false);
106+
};
107+
if elapsed > timeout {
108+
return Ok(true);
109+
}
110+
Ok(false)
111+
}
112+
}
113+
81114
/// Captures the configuration settings used by the signer when evaluating block proposals.
82115
#[derive(Debug, Clone)]
83116
pub struct ProposalEvalConfig {
@@ -156,32 +189,15 @@ impl SortitionsView {
156189
block: &NakamotoBlock,
157190
block_pk: &StacksPublicKey,
158191
) -> Result<bool, SignerChainstateError> {
159-
// If this is the first block in the tenure, check if it was proposed after the timeout
160-
if signer_db
161-
.get_last_signed_block_in_tenure(&block.header.consensus_hash)?
162-
.is_none()
192+
if self
193+
.cur_sortition
194+
.is_timed_out(self.config.block_proposal_timeout, signer_db)?
163195
{
164-
if let Some(received_ts) =
165-
signer_db.get_burn_block_receive_time(&self.cur_sortition.burn_block_hash)?
166-
{
167-
let received_time = UNIX_EPOCH + Duration::from_secs(received_ts);
168-
let elapsed = std::time::SystemTime::now()
169-
.duration_since(received_time)
170-
.unwrap_or_else(|_| {
171-
panic!("Failed to calculate time since burn block received")
172-
});
173-
if elapsed >= self.config.block_proposal_timeout {
174-
warn!(
175-
"Miner proposed first block after block proposal timeout.";
176-
"proposed_block_consensus_hash" => %block.header.consensus_hash,
177-
"proposed_block_signer_sighash" => %block.header.signer_signature_hash(),
178-
"current_sortition_consensus_hash" => ?self.cur_sortition.consensus_hash,
179-
"last_sortition_consensus_hash" => ?self.last_sortition.as_ref().map(|x| x.consensus_hash),
180-
"burn_block_received_time" => ?received_time,
181-
);
182-
self.cur_sortition.miner_status =
183-
SortitionMinerStatus::InvalidatedBeforeFirstBlock;
184-
}
196+
self.cur_sortition.miner_status = SortitionMinerStatus::InvalidatedBeforeFirstBlock;
197+
}
198+
if let Some(last_sortition) = self.last_sortition.as_mut() {
199+
if last_sortition.is_timed_out(self.config.block_proposal_timeout, signer_db)? {
200+
last_sortition.miner_status = SortitionMinerStatus::InvalidatedBeforeFirstBlock;
185201
}
186202
}
187203
let bitvec_all_1s = block.header.pox_treatment.iter().all(|entry| entry);

stacks-signer/src/tests/chainstate.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,3 +387,80 @@ fn check_block_proposal_timeout() {
387387
.check_proposal(&stacks_client, &signer_db, &last_sortition_block, &block_pk)
388388
.unwrap());
389389
}
390+
391+
#[test]
392+
fn check_sortition_timeout() {
393+
let signer_db_dir = "/tmp/stacks-node-tests/signer-units/";
394+
let signer_db_path = format!(
395+
"{signer_db_dir}/sortition_timeout.{}.sqlite",
396+
get_epoch_time_secs()
397+
);
398+
fs::create_dir_all(signer_db_dir).unwrap();
399+
let mut signer_db = SignerDb::new(signer_db_path).unwrap();
400+
401+
let mut sortition = SortitionState {
402+
miner_pkh: Hash160([0; 20]),
403+
miner_pubkey: None,
404+
prior_sortition: ConsensusHash([0; 20]),
405+
parent_tenure_id: ConsensusHash([0; 20]),
406+
consensus_hash: ConsensusHash([1; 20]),
407+
miner_status: SortitionMinerStatus::Valid,
408+
burn_header_timestamp: 2,
409+
burn_block_hash: BurnchainHeaderHash([1; 32]),
410+
};
411+
// Ensure we have a burn height to compare against
412+
let burn_hash = sortition.burn_block_hash;
413+
let burn_height = 1;
414+
let received_time = SystemTime::now();
415+
signer_db
416+
.insert_burn_block(&burn_hash, burn_height, &received_time)
417+
.unwrap();
418+
419+
std::thread::sleep(Duration::from_secs(1));
420+
// We have not yet timed out
421+
assert!(!sortition
422+
.is_timed_out(Duration::from_secs(10), &signer_db)
423+
.unwrap());
424+
// We are a valid sortition, have an empty tenure, and have now timed out
425+
assert!(sortition
426+
.is_timed_out(Duration::from_secs(1), &signer_db)
427+
.unwrap());
428+
// This will not be marked as timed out as the status is no longer valid
429+
sortition.miner_status = SortitionMinerStatus::InvalidatedAfterFirstBlock;
430+
assert!(!sortition
431+
.is_timed_out(Duration::from_secs(1), &signer_db)
432+
.unwrap());
433+
434+
// Revert the status to continue other checks
435+
sortition.miner_status = SortitionMinerStatus::Valid;
436+
// Insert a signed over block so its no longer an empty tenure
437+
let block_proposal = BlockProposal {
438+
block: NakamotoBlock {
439+
header: NakamotoBlockHeader {
440+
version: 1,
441+
chain_length: 10,
442+
burn_spent: 10,
443+
consensus_hash: sortition.consensus_hash,
444+
parent_block_id: StacksBlockId([0; 32]),
445+
tx_merkle_root: Sha512Trunc256Sum([0; 32]),
446+
state_index_root: TrieHash([0; 32]),
447+
timestamp: 11,
448+
miner_signature: MessageSignature::empty(),
449+
signer_signature: vec![],
450+
pox_treatment: BitVec::ones(1).unwrap(),
451+
},
452+
txs: vec![],
453+
},
454+
burn_height: 2,
455+
reward_cycle: 1,
456+
};
457+
458+
let mut block_info = BlockInfo::from(block_proposal);
459+
block_info.signed_over = true;
460+
signer_db.insert_block(&block_info).unwrap();
461+
462+
// This will no longer be timed out as we have a non-empty tenure
463+
assert!(!sortition
464+
.is_timed_out(Duration::from_secs(1), &signer_db)
465+
.unwrap());
466+
}

0 commit comments

Comments
 (0)