Skip to content

Commit 2456645

Browse files
authored
Merge pull request #5425 from stacks-network/feat/signer-times-out-end-of-tenure-blocks
Add config option tenure_last_block_proposal_timeout_secs and do not allow reorgs at tenure boundary before it is exceeded
2 parents ef668ab + d9485b6 commit 2456645

File tree

10 files changed

+332
-27
lines changed

10 files changed

+332
-27
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ jobs:
114114
- tests::signer::v0::locally_accepted_blocks_overriden_by_global_rejection
115115
- tests::signer::v0::locally_rejected_blocks_overriden_by_global_acceptance
116116
- tests::signer::v0::reorg_locally_accepted_blocks_across_tenures_succeeds
117+
- tests::signer::v0::reorg_locally_accepted_blocks_across_tenures_fails
117118
- tests::signer::v0::miner_recovers_when_broadcast_block_delay_across_tenures_occurs
118119
- tests::signer::v0::multiple_miners_with_nakamoto_blocks
119120
- tests::signer::v0::partial_tenure_fork
@@ -135,6 +136,7 @@ jobs:
135136
- tests::nakamoto_integrations::utxo_check_on_startup_panic
136137
- tests::nakamoto_integrations::utxo_check_on_startup_recover
137138
- tests::nakamoto_integrations::v3_signer_api_endpoint
139+
- tests::nakamoto_integrations::signer_chainstate
138140
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
139141
# - tests::signer::v1::dkg
140142
# - tests::signer::v1::sign_request_rejected

stacks-signer/src/chainstate.rs

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
1919
use blockstack_lib::chainstate::stacks::TenureChangePayload;
2020
use blockstack_lib::net::api::getsortition::SortitionInfo;
2121
use blockstack_lib::util_lib::db::Error as DBError;
22-
use clarity::types::chainstate::BurnchainHeaderHash;
2322
use slog::{slog_info, slog_warn};
24-
use stacks_common::types::chainstate::{ConsensusHash, StacksPublicKey};
23+
use stacks_common::types::chainstate::{BurnchainHeaderHash, ConsensusHash, StacksPublicKey};
24+
use stacks_common::util::get_epoch_time_secs;
2525
use stacks_common::util::hash::Hash160;
2626
use stacks_common::{info, warn};
2727

2828
use crate::client::{ClientError, CurrentAndLastSortition, StacksClient};
2929
use crate::config::SignerConfig;
30-
use crate::signerdb::{BlockState, SignerDb};
30+
use crate::signerdb::{BlockInfo, BlockState, SignerDb};
3131

3232
#[derive(thiserror::Error, Debug)]
3333
/// Error type for the signer chainstate module
@@ -119,13 +119,17 @@ pub struct ProposalEvalConfig {
119119
pub first_proposal_burn_block_timing: Duration,
120120
/// Time between processing a sortition and proposing a block before the block is considered invalid
121121
pub block_proposal_timeout: Duration,
122+
/// Time to wait for the last block of a tenure to be globally accepted or rejected before considering
123+
/// a new miner's block at the same height as valid.
124+
pub tenure_last_block_proposal_timeout: Duration,
122125
}
123126

124127
impl From<&SignerConfig> for ProposalEvalConfig {
125128
fn from(value: &SignerConfig) -> Self {
126129
Self {
127130
first_proposal_burn_block_timing: value.first_proposal_burn_block_timing,
128131
block_proposal_timeout: value.block_proposal_timeout,
132+
tenure_last_block_proposal_timeout: value.tenure_last_block_proposal_timeout,
129133
}
130134
}
131135
}
@@ -460,7 +464,36 @@ impl SortitionsView {
460464
Ok(true)
461465
}
462466

463-
/// Check if the tenure change block confirms the expected parent block (i.e., the last globally accepted block in the parent tenure)
467+
/// Get the last block from the given tenure
468+
/// Returns the last locally accepted block if it is not timed out, otherwise it will return the last globally accepted block.
469+
fn get_tenure_last_block_info(
470+
consensus_hash: &ConsensusHash,
471+
signer_db: &SignerDb,
472+
tenure_last_block_proposal_timeout: Duration,
473+
) -> Result<Option<BlockInfo>, ClientError> {
474+
// Get the last known block in the previous tenure
475+
let last_locally_accepted_block = signer_db
476+
.get_last_accepted_block(consensus_hash)
477+
.map_err(|e| ClientError::InvalidResponse(e.to_string()))?;
478+
479+
if let Some(local_info) = last_locally_accepted_block {
480+
if let Some(signed_over_time) = local_info.signed_self {
481+
if signed_over_time + tenure_last_block_proposal_timeout.as_secs()
482+
> get_epoch_time_secs()
483+
{
484+
// The last locally accepted block is not timed out, return it
485+
return Ok(Some(local_info));
486+
}
487+
}
488+
}
489+
// The last locally accepted block is timed out, get the last globally accepted block
490+
signer_db
491+
.get_last_globally_accepted_block(consensus_hash)
492+
.map_err(|e| ClientError::InvalidResponse(e.to_string()))
493+
}
494+
495+
/// Check if the tenure change block confirms the expected parent block
496+
/// (i.e., the last locally accepted block in the parent tenure, or if that block is timed out, the last globally accepted block in the parent tenure)
464497
/// It checks the local DB first, and if the block is not present in the local DB, it asks the
465498
/// Stacks node for the highest processed block header in the given tenure (and then caches it
466499
/// in the DB).
@@ -473,24 +506,27 @@ impl SortitionsView {
473506
reward_cycle: u64,
474507
signer_db: &mut SignerDb,
475508
client: &StacksClient,
509+
tenure_last_block_proposal_timeout: Duration,
476510
) -> Result<bool, ClientError> {
477-
// If the tenure change block confirms the expected parent block, it should confirm at least one more block than the last globally accepted block in the parent tenure.
478-
let last_globally_accepted_block = signer_db
479-
.get_last_globally_accepted_block(&tenure_change.prev_tenure_consensus_hash)
480-
.map_err(|e| ClientError::InvalidResponse(e.to_string()))?;
511+
// If the tenure change block confirms the expected parent block, it should confirm at least one more block than the last accepted block in the parent tenure.
512+
let last_block_info = Self::get_tenure_last_block_info(
513+
&tenure_change.prev_tenure_consensus_hash,
514+
signer_db,
515+
tenure_last_block_proposal_timeout,
516+
)?;
481517

482-
if let Some(global_info) = last_globally_accepted_block {
518+
if let Some(info) = last_block_info {
483519
// N.B. this block might not be the last globally accepted block across the network;
484520
// it's just the highest one in this tenure that we know about. If this given block is
485521
// no higher than it, then it's definitely no higher than the last globally accepted
486522
// block across the network, so we can do an early rejection here.
487-
if block.header.chain_length <= global_info.block.header.chain_length {
523+
if block.header.chain_length <= info.block.header.chain_length {
488524
warn!(
489525
"Miner's block proposal does not confirm as many blocks as we expect";
490526
"proposed_block_consensus_hash" => %block.header.consensus_hash,
491527
"proposed_block_signer_sighash" => %block.header.signer_signature_hash(),
492528
"proposed_chain_length" => block.header.chain_length,
493-
"expected_at_least" => global_info.block.header.chain_length + 1,
529+
"expected_at_least" => info.block.header.chain_length + 1,
494530
);
495531
return Ok(false);
496532
}
@@ -558,6 +594,7 @@ impl SortitionsView {
558594
reward_cycle,
559595
signer_db,
560596
client,
597+
self.config.tenure_last_block_proposal_timeout,
561598
)?;
562599
if !confirms_expected_parent {
563600
return Ok(false);
@@ -573,15 +610,15 @@ impl SortitionsView {
573610
if !is_valid_parent_tenure {
574611
return Ok(false);
575612
}
576-
let last_in_tenure = signer_db
613+
let last_in_current_tenure = signer_db
577614
.get_last_globally_accepted_block(&block.header.consensus_hash)
578615
.map_err(|e| ClientError::InvalidResponse(e.to_string()))?;
579-
if let Some(last_in_tenure) = last_in_tenure {
616+
if let Some(last_in_current_tenure) = last_in_current_tenure {
580617
warn!(
581618
"Miner block proposal contains a tenure change, but we've already signed a block in this tenure. Considering proposal invalid.";
582619
"proposed_block_consensus_hash" => %block.header.consensus_hash,
583620
"proposed_block_signer_sighash" => %block.header.signer_signature_hash(),
584-
"last_in_tenure_signer_sighash" => %last_in_tenure.block.header.signer_signature_hash(),
621+
"last_in_tenure_signer_sighash" => %last_in_current_tenure.block.header.signer_signature_hash(),
585622
);
586623
return Ok(false);
587624
}

stacks-signer/src/client/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ pub(crate) mod tests {
411411
db_path: config.db_path.clone(),
412412
first_proposal_burn_block_timing: config.first_proposal_burn_block_timing,
413413
block_proposal_timeout: config.block_proposal_timeout,
414+
tenure_last_block_proposal_timeout: config.tenure_last_block_proposal_timeout,
414415
}
415416
}
416417

stacks-signer/src/config.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::client::SignerSlotID;
3636
const EVENT_TIMEOUT_MS: u64 = 5000;
3737
const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 600_000;
3838
const DEFAULT_FIRST_PROPOSAL_BURN_BLOCK_TIMING_SECS: u64 = 60;
39+
const DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS: u64 = 30;
3940

4041
#[derive(thiserror::Error, Debug)]
4142
/// An error occurred parsing the provided configuration
@@ -128,6 +129,9 @@ pub struct SignerConfig {
128129
pub first_proposal_burn_block_timing: Duration,
129130
/// How much time to wait for a miner to propose a block following a sortition
130131
pub block_proposal_timeout: Duration,
132+
/// Time to wait for the last block of a tenure to be globally accepted or rejected
133+
/// before considering a new miner's block at the same height as potentially valid.
134+
pub tenure_last_block_proposal_timeout: Duration,
131135
}
132136

133137
/// The parsed configuration for the signer
@@ -158,6 +162,9 @@ pub struct GlobalConfig {
158162
pub block_proposal_timeout: Duration,
159163
/// An optional custom Chain ID
160164
pub chain_id: Option<u32>,
165+
/// Time to wait for the last block of a tenure to be globally accepted or rejected
166+
/// before considering a new miner's block at the same height as potentially valid.
167+
pub tenure_last_block_proposal_timeout: Duration,
161168
}
162169

163170
/// Internal struct for loading up the config file
@@ -180,13 +187,16 @@ struct RawConfigFile {
180187
pub db_path: String,
181188
/// Metrics endpoint
182189
pub metrics_endpoint: Option<String>,
183-
/// How much time must pass between the first block proposal in a tenure and the next bitcoin block
190+
/// How much time must pass in seconds between the first block proposal in a tenure and the next bitcoin block
184191
/// before a subsequent miner isn't allowed to reorg the tenure
185192
pub first_proposal_burn_block_timing_secs: Option<u64>,
186193
/// How much time to wait for a miner to propose a block following a sortition in milliseconds
187194
pub block_proposal_timeout_ms: Option<u64>,
188195
/// An optional custom Chain ID
189196
pub chain_id: Option<u32>,
197+
/// Time in seconds to wait for the last block of a tenure to be globally accepted or rejected
198+
/// before considering a new miner's block at the same height as potentially valid.
199+
pub tenure_last_block_proposal_timeout_secs: Option<u64>,
190200
}
191201

192202
impl RawConfigFile {
@@ -266,6 +276,12 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
266276
.unwrap_or(BLOCK_PROPOSAL_TIMEOUT_MS),
267277
);
268278

279+
let tenure_last_block_proposal_timeout = Duration::from_secs(
280+
raw_data
281+
.tenure_last_block_proposal_timeout_secs
282+
.unwrap_or(DEFAULT_TENURE_LAST_BLOCK_PROPOSAL_TIMEOUT_SECS),
283+
);
284+
269285
Ok(Self {
270286
node_host: raw_data.node_host,
271287
endpoint,
@@ -279,6 +295,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
279295
first_proposal_burn_block_timing,
280296
block_proposal_timeout,
281297
chain_id: raw_data.chain_id,
298+
tenure_last_block_proposal_timeout,
282299
})
283300
}
284301
}

stacks-signer/src/runloop.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
283283
mainnet: self.config.network.is_mainnet(),
284284
db_path: self.config.db_path.clone(),
285285
block_proposal_timeout: self.config.block_proposal_timeout,
286+
tenure_last_block_proposal_timeout: self.config.tenure_last_block_proposal_timeout,
286287
}))
287288
}
288289

stacks-signer/src/signerdb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ use blockstack_lib::util_lib::db::{
2424
Error as DBError,
2525
};
2626
use clarity::types::chainstate::{BurnchainHeaderHash, StacksAddress};
27-
use clarity::util::get_epoch_time_secs;
2827
use libsigner::BlockProposal;
2928
use rusqlite::{
3029
params, Connection, Error as SqliteError, OpenFlags, OptionalExtension, Transaction,
@@ -33,6 +32,7 @@ use serde::{Deserialize, Serialize};
3332
use slog::{slog_debug, slog_error};
3433
use stacks_common::codec::{read_next, write_next, Error as CodecError, StacksMessageCodec};
3534
use stacks_common::types::chainstate::ConsensusHash;
35+
use stacks_common::util::get_epoch_time_secs;
3636
use stacks_common::util::hash::Sha512Trunc256Sum;
3737
use stacks_common::util::secp256k1::MessageSignature;
3838
use stacks_common::{debug, define_u8_enum, error};

stacks-signer/src/tests/chainstate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ fn setup_test_environment(
8989
config: ProposalEvalConfig {
9090
first_proposal_burn_block_timing: Duration::from_secs(30),
9191
block_proposal_timeout: Duration::from_secs(5),
92+
tenure_last_block_proposal_timeout: Duration::from_secs(30),
9293
},
9394
};
9495

stackslib/src/chainstate/stacks/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1101,13 +1101,12 @@ pub const MAX_MICROBLOCK_SIZE: u32 = 65536;
11011101

11021102
#[cfg(test)]
11031103
pub mod test {
1104-
use clarity::util::get_epoch_time_secs;
11051104
use clarity::vm::representations::{ClarityName, ContractName};
11061105
use clarity::vm::ClarityVersion;
11071106
use stacks_common::bitvec::BitVec;
11081107
use stacks_common::util::hash::*;
1109-
use stacks_common::util::log;
11101108
use stacks_common::util::secp256k1::Secp256k1PrivateKey;
1109+
use stacks_common::util::{get_epoch_time_secs, log};
11111110

11121111
use super::*;
11131112
use crate::chainstate::burn::BlockSnapshot;

testnet/stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6369,6 +6369,7 @@ fn signer_chainstate() {
63696369
let proposal_conf = ProposalEvalConfig {
63706370
first_proposal_burn_block_timing: Duration::from_secs(0),
63716371
block_proposal_timeout: Duration::from_secs(100),
6372+
tenure_last_block_proposal_timeout: Duration::from_secs(30),
63726373
};
63736374
let mut sortitions_view =
63746375
SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap();
@@ -6507,6 +6508,7 @@ fn signer_chainstate() {
65076508
let proposal_conf = ProposalEvalConfig {
65086509
first_proposal_burn_block_timing: Duration::from_secs(0),
65096510
block_proposal_timeout: Duration::from_secs(100),
6511+
tenure_last_block_proposal_timeout: Duration::from_secs(30),
65106512
};
65116513
let burn_block_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())
65126514
.unwrap()
@@ -6541,10 +6543,10 @@ fn signer_chainstate() {
65416543
valid: Some(true),
65426544
signed_over: true,
65436545
proposed_time: get_epoch_time_secs(),
6544-
signed_self: None,
6545-
signed_group: None,
6546+
signed_self: Some(get_epoch_time_secs()),
6547+
signed_group: Some(get_epoch_time_secs()),
65466548
ext: ExtraBlockInfo::None,
6547-
state: BlockState::Unprocessed,
6549+
state: BlockState::GloballyAccepted,
65486550
})
65496551
.unwrap();
65506552

@@ -6584,6 +6586,7 @@ fn signer_chainstate() {
65846586
let proposal_conf = ProposalEvalConfig {
65856587
first_proposal_burn_block_timing: Duration::from_secs(0),
65866588
block_proposal_timeout: Duration::from_secs(100),
6589+
tenure_last_block_proposal_timeout: Duration::from_secs(30),
65876590
};
65886591
let mut sortitions_view = SortitionsView::fetch_view(proposal_conf, &signer_client).unwrap();
65896592
let burn_block_height = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn())

0 commit comments

Comments
 (0)