Skip to content

Commit 7160418

Browse files
authored
Merge branch 'develop' into feat/clarity-wasm-develop
2 parents 5a3121a + 98b7d3d commit 7160418

File tree

25 files changed

+1214
-242
lines changed

25 files changed

+1214
-242
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ jobs:
8989
- tests::signer::v0::end_of_tenure
9090
- tests::signer::v0::forked_tenure_okay
9191
- tests::signer::v0::forked_tenure_invalid
92+
- tests::signer::v0::empty_sortition
9293
- tests::signer::v0::bitcoind_forking_test
9394
- tests::nakamoto_integrations::stack_stx_burn_op_integration_test
9495
- tests::nakamoto_integrations::check_block_heights
@@ -97,6 +98,7 @@ jobs:
9798
- tests::nakamoto_integrations::check_block_info
9899
- tests::nakamoto_integrations::check_block_info_rewards
99100
- tests::nakamoto_integrations::continue_tenure_extend
101+
- tests::nakamoto_integrations::multiple_miners
100102
# Do not run this one until we figure out why it fails in CI
101103
# - tests::neon_integrations::bitcoin_reorg_flap
102104
# - tests::neon_integrations::bitcoin_reorg_flap_with_follower

stacks-signer/src/chainstate.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// You should have received a copy of the GNU General Public License
1414
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1515

16-
use std::time::Duration;
16+
use std::time::{Duration, UNIX_EPOCH};
1717

1818
use blockstack_lib::chainstate::nakamoto::NakamotoBlock;
1919
use blockstack_lib::chainstate::stacks::TenureChangePayload;
@@ -78,18 +78,54 @@ 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 {
84117
/// How much time must pass between the first block proposal in a tenure and the next bitcoin block
85118
/// before a subsequent miner isn't allowed to reorg the tenure
86119
pub first_proposal_burn_block_timing: Duration,
120+
/// Time between processing a sortition and proposing a block before the block is considered invalid
121+
pub block_proposal_timeout: Duration,
87122
}
88123

89124
impl From<&SignerConfig> for ProposalEvalConfig {
90125
fn from(value: &SignerConfig) -> Self {
91126
Self {
92-
first_proposal_burn_block_timing: value.first_proposal_burn_block_timing.clone(),
127+
first_proposal_burn_block_timing: value.first_proposal_burn_block_timing,
128+
block_proposal_timeout: value.block_proposal_timeout,
93129
}
94130
}
95131
}
@@ -147,12 +183,23 @@ impl<'a> ProposedBy<'a> {
147183
impl SortitionsView {
148184
/// Apply checks from the SortitionsView on the block proposal.
149185
pub fn check_proposal(
150-
&self,
186+
&mut self,
151187
client: &StacksClient,
152188
signer_db: &SignerDb,
153189
block: &NakamotoBlock,
154190
block_pk: &StacksPublicKey,
155191
) -> Result<bool, SignerChainstateError> {
192+
if self
193+
.cur_sortition
194+
.is_timed_out(self.config.block_proposal_timeout, signer_db)?
195+
{
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;
201+
}
202+
}
156203
let bitvec_all_1s = block.header.pox_treatment.iter().all(|entry| entry);
157204
if !bitvec_all_1s {
158205
warn!(

stacks-signer/src/cli.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use clarity::util::hash::Sha256Sum;
2929
use clarity::util::secp256k1::MessageSignature;
3030
use clarity::vm::types::{QualifiedContractIdentifier, TupleData};
3131
use clarity::vm::Value;
32+
use lazy_static::lazy_static;
3233
use serde::{Deserialize, Serialize};
3334
use stacks_common::address::{
3435
b58, AddressHashMode, C32_ADDRESS_VERSION_MAINNET_MULTISIG,
@@ -40,8 +41,34 @@ use stacks_common::types::chainstate::StacksPrivateKey;
4041

4142
extern crate alloc;
4243

44+
const GIT_BRANCH: Option<&'static str> = option_env!("GIT_BRANCH");
45+
const GIT_COMMIT: Option<&'static str> = option_env!("GIT_COMMIT");
46+
#[cfg(debug_assertions)]
47+
const BUILD_TYPE: &'static str = "debug";
48+
#[cfg(not(debug_assertions))]
49+
const BUILD_TYPE: &'static str = "release";
50+
51+
lazy_static! {
52+
static ref VERSION_STRING: String = {
53+
let pkg_version = option_env!("STACKS_NODE_VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
54+
let git_branch = GIT_BRANCH.unwrap_or("");
55+
let git_commit = GIT_COMMIT.unwrap_or("");
56+
format!(
57+
"{} ({}:{}, {} build, {} [{}])",
58+
pkg_version,
59+
git_branch,
60+
git_commit,
61+
BUILD_TYPE,
62+
std::env::consts::OS,
63+
std::env::consts::ARCH
64+
)
65+
};
66+
}
67+
4368
#[derive(Parser, Debug)]
4469
#[command(author, version, about)]
70+
#[command(long_version = VERSION_STRING.as_str())]
71+
4572
/// The CLI arguments for the stacks signer
4673
pub struct Cli {
4774
/// Subcommand action to take

stacks-signer/src/client/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,8 @@ pub(crate) mod tests {
565565
tx_fee_ustx: config.tx_fee_ustx,
566566
max_tx_fee_ustx: config.max_tx_fee_ustx,
567567
db_path: config.db_path.clone(),
568-
first_proposal_burn_block_timing: Duration::from_secs(30),
568+
first_proposal_burn_block_timing: config.first_proposal_burn_block_timing,
569+
block_proposal_timeout: config.block_proposal_timeout,
569570
}
570571
}
571572

stacks-signer/src/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use wsts::curve::scalar::Scalar;
3636
use crate::client::SignerSlotID;
3737

3838
const EVENT_TIMEOUT_MS: u64 = 5000;
39+
const BLOCK_PROPOSAL_TIMEOUT_MS: u64 = 45_000;
3940
// Default transaction fee to use in microstacks (if unspecificed in the config file)
4041
const TX_FEE_USTX: u64 = 10_000;
4142

@@ -154,6 +155,8 @@ pub struct SignerConfig {
154155
/// How much time must pass between the first block proposal in a tenure and the next bitcoin block
155156
/// before a subsequent miner isn't allowed to reorg the tenure
156157
pub first_proposal_burn_block_timing: Duration,
158+
/// How much time to wait for a miner to propose a block following a sortition
159+
pub block_proposal_timeout: Duration,
157160
}
158161

159162
/// The parsed configuration for the signer
@@ -196,6 +199,8 @@ pub struct GlobalConfig {
196199
/// How much time between the first block proposal in a tenure and the next bitcoin block
197200
/// must pass before a subsequent miner isn't allowed to reorg the tenure
198201
pub first_proposal_burn_block_timing: Duration,
202+
/// How much time to wait for a miner to propose a block following a sortition
203+
pub block_proposal_timeout: Duration,
199204
}
200205

201206
/// Internal struct for loading up the config file
@@ -236,6 +241,8 @@ struct RawConfigFile {
236241
/// How much time must pass between the first block proposal in a tenure and the next bitcoin block
237242
/// before a subsequent miner isn't allowed to reorg the tenure
238243
pub first_proposal_burn_block_timing_secs: Option<u64>,
244+
/// How much time to wait for a miner to propose a block following a sortition in milliseconds
245+
pub block_proposal_timeout_ms: Option<u64>,
239246
}
240247

241248
impl RawConfigFile {
@@ -324,6 +331,12 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
324331
None => None,
325332
};
326333

334+
let block_proposal_timeout = Duration::from_millis(
335+
raw_data
336+
.block_proposal_timeout_ms
337+
.unwrap_or(BLOCK_PROPOSAL_TIMEOUT_MS),
338+
);
339+
327340
Ok(Self {
328341
node_host: raw_data.node_host,
329342
endpoint,
@@ -343,6 +356,7 @@ impl TryFrom<RawConfigFile> for GlobalConfig {
343356
db_path,
344357
metrics_endpoint,
345358
first_proposal_burn_block_timing,
359+
block_proposal_timeout,
346360
})
347361
}
348362
}

stacks-signer/src/runloop.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
270270
tx_fee_ustx: self.config.tx_fee_ustx,
271271
max_tx_fee_ustx: self.config.max_tx_fee_ustx,
272272
db_path: self.config.db_path.clone(),
273+
block_proposal_timeout: self.config.block_proposal_timeout,
273274
})
274275
}
275276

@@ -365,7 +366,7 @@ impl<Signer: SignerTrait<T>, T: StacksMessageCodec + Clone + Send + Debug> RunLo
365366
self.refresh_signer_config(next_reward_cycle);
366367
}
367368
} else {
368-
debug!("Received a new burnchain block height ({current_burn_block_height}) but not in prepare phase.";
369+
info!("Received a new burnchain block height ({current_burn_block_height}) but not in prepare phase.";
369370
"reward_cycle" => reward_cycle_info.reward_cycle,
370371
"reward_cycle_length" => reward_cycle_info.reward_cycle_length,
371372
"prepare_phase_block_length" => reward_cycle_info.prepare_phase_block_length,

stacks-signer/src/signerdb.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ impl SignerDb {
339339
tenure: &ConsensusHash,
340340
) -> Result<Option<BlockInfo>, DBError> {
341341
let query = "SELECT block_info FROM blocks WHERE consensus_hash = ? AND signed_over = 1 ORDER BY stacks_height ASC LIMIT 1";
342-
let result: Option<String> = query_row(&self.db, query, &[tenure])?;
342+
let result: Option<String> = query_row(&self.db, query, [tenure])?;
343343

344344
try_deserialize(result)
345345
}

stacks-signer/src/tests/chainstate.rs

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ fn setup_test_environment(
8686
last_sortition,
8787
config: ProposalEvalConfig {
8888
first_proposal_burn_block_timing: Duration::from_secs(30),
89+
block_proposal_timeout: Duration::from_secs(5),
8990
},
9091
};
9192

@@ -110,7 +111,7 @@ fn setup_test_environment(
110111
parent_block_id: StacksBlockId([0; 32]),
111112
tx_merkle_root: Sha512Trunc256Sum([0; 32]),
112113
state_index_root: TrieHash([0; 32]),
113-
timestamp: 11,
114+
timestamp: 3,
114115
miner_signature: MessageSignature::empty(),
115116
signer_signature: vec![],
116117
pox_treatment: BitVec::ones(1).unwrap(),
@@ -139,7 +140,7 @@ fn check_proposal_units() {
139140

140141
#[test]
141142
fn check_proposal_miner_pkh_mismatch() {
142-
let (stacks_client, signer_db, _block_pk, view, mut block) =
143+
let (stacks_client, signer_db, _block_pk, mut view, mut block) =
143144
setup_test_environment("miner_pkh_mismatch");
144145
block.header.consensus_hash = view.cur_sortition.consensus_hash;
145146
let different_block_pk = StacksPublicKey::from_private(&StacksPrivateKey::from_seed(&[2, 3]));
@@ -327,7 +328,7 @@ fn make_tenure_change_tx(payload: TenureChangePayload) -> StacksTransaction {
327328

328329
#[test]
329330
fn check_proposal_tenure_extend_invalid_conditions() {
330-
let (stacks_client, signer_db, block_pk, view, mut block) =
331+
let (stacks_client, signer_db, block_pk, mut view, mut block) =
331332
setup_test_environment("tenure_extend");
332333
block.header.consensus_hash = view.cur_sortition.consensus_hash;
333334
let mut extend_payload = make_tenure_change_payload();
@@ -350,3 +351,116 @@ fn check_proposal_tenure_extend_invalid_conditions() {
350351
.check_proposal(&stacks_client, &signer_db, &block, &block_pk)
351352
.unwrap());
352353
}
354+
355+
#[test]
356+
fn check_block_proposal_timeout() {
357+
let (stacks_client, mut signer_db, block_pk, mut view, mut curr_sortition_block) =
358+
setup_test_environment("block_proposal_timeout");
359+
curr_sortition_block.header.consensus_hash = view.cur_sortition.consensus_hash;
360+
let mut last_sortition_block = curr_sortition_block.clone();
361+
last_sortition_block.header.consensus_hash =
362+
view.last_sortition.as_ref().unwrap().consensus_hash;
363+
364+
// Ensure we have a burn height to compare against
365+
let burn_hash = view.cur_sortition.burn_block_hash;
366+
let burn_height = 1;
367+
let received_time = SystemTime::now();
368+
signer_db
369+
.insert_burn_block(&burn_hash, burn_height, &received_time)
370+
.unwrap();
371+
372+
assert!(view
373+
.check_proposal(&stacks_client, &signer_db, &curr_sortition_block, &block_pk)
374+
.unwrap());
375+
376+
assert!(!view
377+
.check_proposal(&stacks_client, &signer_db, &last_sortition_block, &block_pk)
378+
.unwrap());
379+
380+
// Sleep a bit to time out the block proposal
381+
std::thread::sleep(Duration::from_secs(5));
382+
assert!(!view
383+
.check_proposal(&stacks_client, &signer_db, &curr_sortition_block, &block_pk)
384+
.unwrap());
385+
386+
assert!(view
387+
.check_proposal(&stacks_client, &signer_db, &last_sortition_block, &block_pk)
388+
.unwrap());
389+
}
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)