Skip to content

Commit 6228b9b

Browse files
authored
Merge pull request #5787 from stacks-network/feat/extend-when-needed
feat: do not issue a time-based tenure extend earlier than needed
2 parents 7e8f2a3 + 6fccd18 commit 6228b9b

File tree

8 files changed

+264
-41
lines changed

8 files changed

+264
-41
lines changed

CHANGELOG.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,13 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
88
## [Unreleased]
99

1010
### Added
11-
- Add `dry_run` configuration option to `stacks-signer` config toml. Dry run mode will
12-
run the signer binary as if it were a registered signer. Instead of broadcasting
13-
`StackerDB` messages, it logs `INFO` messages. Other interactions with the `stacks-node`
14-
behave normally (e.g., submitting validation requests, submitting finished blocks). A
15-
dry run signer will error out if the supplied key is actually a registered signer.
11+
12+
- Add miner configuration option `tenure_extend_cost_threshold` to specify the percentage of the tenure budget that must be spent before a time-based tenure extend is attempted
1613

1714
### Changed
1815

1916
- Miner will include other transactions in blocks with tenure extend transactions (#5760)
17+
- Miner will not issue a tenure extend until at least half of the block budget has been spent (#5757)
2018

2119
### Fixed
2220

stacks-signer/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to the versioning scheme outlined in the [README.md](README.md).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Add `dry_run` configuration option to `stacks-signer` config toml. Dry run mode will
13+
run the signer binary as if it were a registered signer. Instead of broadcasting
14+
`StackerDB` messages, it logs `INFO` messages. Other interactions with the `stacks-node`
15+
behave normally (e.g., submitting validation requests, submitting finished blocks). A
16+
dry run signer will error out if the supplied key is actually a registered signer.
17+
818
## [3.1.0.0.4.0]
919

1020
## Added

stackslib/src/chainstate/nakamoto/miner.rs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,21 @@ pub struct MinerTenureInfo<'a> {
150150
pub tenure_block_commit_opt: Option<LeaderBlockCommitOp>,
151151
}
152152

153+
/// Structure returned from `NakamotoBlockBuilder::build_nakamoto_block` with
154+
/// information about the block that was built.
155+
pub struct BlockMetadata {
156+
/// The block that was built
157+
pub block: NakamotoBlock,
158+
/// The execution cost consumed so far by the current tenure
159+
pub tenure_consumed: ExecutionCost,
160+
/// The cost budget for the current tenure
161+
pub tenure_budget: ExecutionCost,
162+
/// The size of the blocks in the current tenure in bytes
163+
pub tenure_size: u64,
164+
/// The events emitted by the transactions included in this block
165+
pub tx_events: Vec<TransactionEvent>,
166+
}
167+
153168
impl NakamotoBlockBuilder {
154169
/// Make a block builder from genesis (testing only)
155170
pub fn new_first_block(
@@ -526,7 +541,7 @@ impl NakamotoBlockBuilder {
526541
settings: BlockBuilderSettings,
527542
event_observer: Option<&dyn MemPoolEventDispatcher>,
528543
signer_bitvec_len: u16,
529-
) -> Result<(NakamotoBlock, ExecutionCost, u64, Vec<TransactionEvent>), Error> {
544+
) -> Result<BlockMetadata, Error> {
530545
let (tip_consensus_hash, tip_block_hash, tip_height) = (
531546
parent_stacks_header.consensus_hash.clone(),
532547
parent_stacks_header.anchored_header.block_hash(),
@@ -556,7 +571,7 @@ impl NakamotoBlockBuilder {
556571
builder.load_tenure_info(&mut chainstate, burn_dbconn, tenure_info.cause())?;
557572
let mut tenure_tx = builder.tenure_begin(burn_dbconn, &mut miner_tenure_info)?;
558573

559-
let block_limit = tenure_tx
574+
let tenure_budget = tenure_tx
560575
.block_limit()
561576
.expect("Failed to obtain block limit from miner's block connection");
562577

@@ -570,7 +585,7 @@ impl NakamotoBlockBuilder {
570585
(1..=100).contains(&percentage),
571586
"BUG: tenure_cost_limit_per_block_percentage: {percentage}%. Must be between between 1 and 100"
572587
);
573-
let mut remaining_limit = block_limit.clone();
588+
let mut remaining_limit = tenure_budget.clone();
574589
let cost_so_far = tenure_tx.cost_so_far();
575590
if remaining_limit.sub(&cost_so_far).is_ok() && remaining_limit.divide(100).is_ok() {
576591
remaining_limit.multiply(percentage.into()).expect(
@@ -581,7 +596,7 @@ impl NakamotoBlockBuilder {
581596
"Setting soft limit for clarity cost to {percentage}% of remaining block limit";
582597
"remaining_limit" => %remaining_limit,
583598
"cost_so_far" => %cost_so_far,
584-
"block_limit" => %block_limit,
599+
"block_limit" => %tenure_budget,
585600
);
586601
soft_limit = Some(remaining_limit);
587602
};
@@ -630,13 +645,13 @@ impl NakamotoBlockBuilder {
630645

631646
// save the block so we can build microblocks off of it
632647
let block = builder.mine_nakamoto_block(&mut tenure_tx);
633-
let size = builder.bytes_so_far;
634-
let consumed = builder.tenure_finish(tenure_tx)?;
648+
let tenure_size = builder.bytes_so_far;
649+
let tenure_consumed = builder.tenure_finish(tenure_tx)?;
635650

636651
let ts_end = get_epoch_time_ms();
637652

638653
set_last_mined_block_transaction_count(block.txs.len() as u64);
639-
set_last_mined_execution_cost_observed(&consumed, &block_limit);
654+
set_last_mined_execution_cost_observed(&tenure_consumed, &tenure_budget);
640655

641656
info!(
642657
"Miner: mined Nakamoto block";
@@ -645,14 +660,20 @@ impl NakamotoBlockBuilder {
645660
"height" => block.header.chain_length,
646661
"tx_count" => block.txs.len(),
647662
"parent_block_id" => %block.header.parent_block_id,
648-
"block_size" => size,
649-
"execution_consumed" => %consumed,
650-
"percent_full" => block_limit.proportion_largest_dimension(&consumed),
663+
"block_size" => tenure_size,
664+
"execution_consumed" => %tenure_consumed,
665+
"percent_full" => tenure_budget.proportion_largest_dimension(&tenure_consumed),
651666
"assembly_time_ms" => ts_end.saturating_sub(ts_start),
652667
"consensus_hash" => %block.header.consensus_hash
653668
);
654669

655-
Ok((block, consumed, size, tx_events))
670+
Ok(BlockMetadata {
671+
block,
672+
tenure_consumed,
673+
tenure_budget,
674+
tenure_size,
675+
tx_events,
676+
})
656677
}
657678

658679
pub fn get_bytes_so_far(&self) -> u64 {

stackslib/src/cli.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ use crate::chainstate::burn::db::sortdb::{
4040
};
4141
use crate::chainstate::burn::{BlockSnapshot, ConsensusHash};
4242
use crate::chainstate::coordinator::OnChainRewardSetProvider;
43-
use crate::chainstate::nakamoto::miner::{NakamotoBlockBuilder, NakamotoTenureInfo};
43+
use crate::chainstate::nakamoto::miner::{BlockMetadata, NakamotoBlockBuilder, NakamotoTenureInfo};
4444
use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoChainState};
4545
use crate::chainstate::stacks::db::blocks::StagingBlock;
4646
use crate::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksChainState, StacksHeaderInfo};
@@ -504,7 +504,21 @@ pub fn command_try_mine(argv: &[String], conf: Option<&Config>) {
504504
None,
505505
0,
506506
)
507-
.map(|(block, cost, size, _)| (block.header.block_hash(), block.txs, cost, size))
507+
.map(
508+
|BlockMetadata {
509+
block,
510+
tenure_consumed,
511+
tenure_size,
512+
..
513+
}| {
514+
(
515+
block.header.block_hash(),
516+
block.txs,
517+
tenure_consumed,
518+
tenure_size,
519+
)
520+
},
521+
)
508522
}
509523
};
510524

stackslib/src/config/mod.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,41 @@ pub const OP_TX_ANY_ESTIM_SIZE: u64 = fmax!(
8686
OP_TX_VOTE_AGG_ESTIM_SIZE
8787
);
8888

89+
/// Default maximum percentage of `satoshis_per_byte` that a Bitcoin fee rate
90+
/// may be increased to when RBFing a transaction
8991
const DEFAULT_MAX_RBF_RATE: u64 = 150; // 1.5x
92+
/// Amount to increment the fee by, in Sats/vByte, when RBFing a Bitcoin
93+
/// transaction
9094
const DEFAULT_RBF_FEE_RATE_INCREMENT: u64 = 5;
95+
/// Default number of reward cycles of blocks to sync in a non-full inventory
96+
/// sync
9197
const INV_REWARD_CYCLES_TESTNET: u64 = 6;
98+
/// Default minimum time to wait between mining blocks in milliseconds. The
99+
/// value must be greater than or equal to 1000 ms because if a block is mined
100+
/// within the same second as its parent, it will be rejected by the signers.
92101
const DEFAULT_MIN_TIME_BETWEEN_BLOCKS_MS: u64 = 1_000;
102+
/// Default time in milliseconds to pause after receiving the first threshold
103+
/// rejection, before proposing a new block.
93104
const DEFAULT_FIRST_REJECTION_PAUSE_MS: u64 = 5_000;
105+
/// Default time in milliseconds to pause after receiving subsequent threshold
106+
/// rejections, before proposing a new block.
94107
const DEFAULT_SUBSEQUENT_REJECTION_PAUSE_MS: u64 = 10_000;
108+
/// Default time in milliseconds to wait for a Nakamoto block after seeing a
109+
/// burnchain block before submitting a block commit.
95110
const DEFAULT_BLOCK_COMMIT_DELAY_MS: u64 = 20_000;
111+
/// Default percentage of the remaining tenure cost limit to consume each block
96112
const DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE: u8 = 25;
113+
/// Default number of seconds to wait in-between polling the sortition DB to
114+
/// see if we need to extend the ongoing tenure (e.g. because the current
115+
/// sortition is empty or invalid).
97116
const DEFAULT_TENURE_EXTEND_POLL_SECS: u64 = 1;
98-
99-
// This should be greater than the signers' timeout. This is used for issuing fallback tenure extends
100-
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 420;
117+
/// Default duration to wait before attempting to issue a tenure extend.
118+
/// This should be greater than the signers' timeout. This is used for issuing
119+
/// fallback tenure extends
120+
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 180;
121+
/// Default percentage of block budget that must be used before attempting a
122+
/// time-based tenure extend
123+
const DEFAULT_TENURE_EXTEND_COST_THRESHOLD: u64 = 50;
101124

102125
static HELIUM_DEFAULT_CONNECTION_OPTIONS: LazyLock<ConnectionOptions> =
103126
LazyLock::new(|| ConnectionOptions {
@@ -1191,9 +1214,13 @@ pub struct BurnchainConfig {
11911214
pub process_exit_at_block_height: Option<u64>,
11921215
pub poll_time_secs: u64,
11931216
pub satoshis_per_byte: u64,
1217+
/// Maximum percentage of `satoshis_per_byte` that a Bitcoin fee rate may
1218+
/// be increased to when RBFing a transaction
11941219
pub max_rbf: u64,
11951220
pub leader_key_tx_estimated_size: u64,
11961221
pub block_commit_tx_estimated_size: u64,
1222+
/// Amount to increment the fee by, in Sats/vByte, when RBFing a Bitcoin
1223+
/// transaction
11971224
pub rbf_fee_increment: u64,
11981225
pub first_burn_block_height: Option<u64>,
11991226
pub first_burn_block_timestamp: Option<u32>,
@@ -2155,6 +2182,8 @@ pub struct MinerConfig {
21552182
pub tenure_extend_poll_secs: Duration,
21562183
/// Duration to wait before attempting to issue a tenure extend
21572184
pub tenure_timeout: Duration,
2185+
/// Percentage of block budget that must be used before attempting a time-based tenure extend
2186+
pub tenure_extend_cost_threshold: u64,
21582187
}
21592188

21602189
impl Default for MinerConfig {
@@ -2193,6 +2222,7 @@ impl Default for MinerConfig {
21932222
),
21942223
tenure_extend_poll_secs: Duration::from_secs(DEFAULT_TENURE_EXTEND_POLL_SECS),
21952224
tenure_timeout: Duration::from_secs(DEFAULT_TENURE_TIMEOUT_SECS),
2225+
tenure_extend_cost_threshold: DEFAULT_TENURE_EXTEND_COST_THRESHOLD,
21962226
}
21972227
}
21982228
}
@@ -2589,6 +2619,7 @@ pub struct MinerConfigFile {
25892619
pub tenure_cost_limit_per_block_percentage: Option<u8>,
25902620
pub tenure_extend_poll_secs: Option<u64>,
25912621
pub tenure_timeout_secs: Option<u64>,
2622+
pub tenure_extend_cost_threshold: Option<u64>,
25922623
}
25932624

25942625
impl MinerConfigFile {
@@ -2731,6 +2762,7 @@ impl MinerConfigFile {
27312762
tenure_cost_limit_per_block_percentage,
27322763
tenure_extend_poll_secs: self.tenure_extend_poll_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_extend_poll_secs),
27332764
tenure_timeout: self.tenure_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_timeout),
2765+
tenure_extend_cost_threshold: self.tenure_extend_cost_threshold.unwrap_or(miner_default_config.tenure_extend_cost_threshold),
27342766
})
27352767
}
27362768
}

stackslib/src/net/connection.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ pub struct ConnectionOptions {
379379
/// Units are milliseconds. A value of 0 means "never".
380380
pub log_neighbors_freq: u64,
381381
pub inv_sync_interval: u64,
382+
// how many reward cycles of blocks to sync in a non-full inventory sync
382383
pub inv_reward_cycles: u64,
383384
pub download_interval: u64,
384385
pub pingback_timeout: u64,

testnet/stacks-node/src/nakamoto_node/miner.rs

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use std::thread;
2121
use std::time::{Duration, Instant};
2222

2323
use clarity::boot_util::boot_code_id;
24+
use clarity::vm::costs::ExecutionCost;
2425
use clarity::vm::types::PrincipalData;
2526
use libsigner::v0::messages::{MinerSlotID, SignerMessage};
2627
use libsigner::StackerDBSession;
@@ -177,6 +178,10 @@ pub struct BlockMinerThread {
177178
last_block_mined: Option<NakamotoBlock>,
178179
/// Number of blocks mined since a tenure change/extend was attempted
179180
mined_blocks: u64,
181+
/// Cost consumed by the current tenure
182+
tenure_cost: ExecutionCost,
183+
/// Cost budget for the current tenure
184+
tenure_budget: ExecutionCost,
180185
/// Copy of the node's registered VRF key
181186
registered_key: RegisteredKey,
182187
/// Burnchain block snapshot which elected this miner
@@ -237,6 +242,8 @@ impl BlockMinerThread {
237242
burn_tip_at_start: burn_tip_at_start.clone(),
238243
tenure_change_time: Instant::now(),
239244
abort_flag: Arc::new(AtomicBool::new(false)),
245+
tenure_cost: ExecutionCost::ZERO,
246+
tenure_budget: ExecutionCost::ZERO,
240247
}
241248
}
242249

@@ -1183,7 +1190,7 @@ impl BlockMinerThread {
11831190
}
11841191

11851192
// build the block itself
1186-
let (mut block, consumed, size, tx_events) = NakamotoBlockBuilder::build_nakamoto_block(
1193+
let mut block_metadata = NakamotoBlockBuilder::build_nakamoto_block(
11871194
&chain_state,
11881195
&burn_db
11891196
.index_handle_at_ch(&self.burn_block.consensus_hash)
@@ -1210,39 +1217,48 @@ impl BlockMinerThread {
12101217
e
12111218
})?;
12121219

1213-
if block.txs.is_empty() {
1220+
if block_metadata.block.txs.is_empty() {
12141221
return Err(ChainstateError::NoTransactionsToMine.into());
12151222
}
12161223
let mining_key = self.keychain.get_nakamoto_sk();
12171224
let miner_signature = mining_key
1218-
.sign(block.header.miner_signature_hash().as_bytes())
1225+
.sign(
1226+
block_metadata
1227+
.block
1228+
.header
1229+
.miner_signature_hash()
1230+
.as_bytes(),
1231+
)
12191232
.map_err(NakamotoNodeError::MinerSignatureError)?;
1220-
block.header.miner_signature = miner_signature;
1233+
block_metadata.block.header.miner_signature = miner_signature;
12211234

12221235
info!(
12231236
"Miner: Assembled block #{} for signer set proposal: {}, with {} txs",
1224-
block.header.chain_length,
1225-
block.header.block_hash(),
1226-
block.txs.len();
1227-
"signer_sighash" => %block.header.signer_signature_hash(),
1228-
"consensus_hash" => %block.header.consensus_hash,
1229-
"parent_block_id" => %block.header.parent_block_id,
1230-
"timestamp" => block.header.timestamp,
1237+
block_metadata.block.header.chain_length,
1238+
block_metadata.block.header.block_hash(),
1239+
block_metadata.block.txs.len();
1240+
"signer_sighash" => %block_metadata.block.header.signer_signature_hash(),
1241+
"consensus_hash" => %block_metadata.block.header.consensus_hash,
1242+
"parent_block_id" => %block_metadata.block.header.parent_block_id,
1243+
"timestamp" => block_metadata.block.header.timestamp,
12311244
);
12321245

12331246
self.event_dispatcher.process_mined_nakamoto_block_event(
12341247
self.burn_block.block_height,
1235-
&block,
1236-
size,
1237-
&consumed,
1238-
tx_events,
1248+
&block_metadata.block,
1249+
block_metadata.tenure_size,
1250+
&block_metadata.tenure_consumed,
1251+
block_metadata.tx_events,
12391252
);
12401253

1254+
self.tenure_cost = block_metadata.tenure_consumed;
1255+
self.tenure_budget = block_metadata.tenure_budget;
1256+
12411257
// last chance -- confirm that the stacks tip is unchanged (since it could have taken long
12421258
// enough to build this block that another block could have arrived), and confirm that all
12431259
// Stacks blocks with heights higher than the canonical tip are processed.
12441260
self.check_burn_tip_changed(&burn_db)?;
1245-
Ok(block)
1261+
Ok(block_metadata.block)
12461262
}
12471263

12481264
#[cfg_attr(test, mutants::skip)]
@@ -1273,8 +1289,20 @@ impl BlockMinerThread {
12731289
}
12741290
}
12751291
};
1292+
// Check if we can and should include a time-based tenure extend.
12761293
if self.last_block_mined.is_some() {
1277-
// Check if we can extend the current tenure
1294+
// Do not extend if we have spent < 50% of the budget, since it is
1295+
// not necessary.
1296+
let usage = self
1297+
.tenure_budget
1298+
.proportion_largest_dimension(&self.tenure_cost);
1299+
if usage < self.config.miner.tenure_extend_cost_threshold {
1300+
return Ok(NakamotoTenureInfo {
1301+
coinbase_tx: None,
1302+
tenure_change_tx: None,
1303+
});
1304+
}
1305+
12781306
let tenure_extend_timestamp = coordinator.get_tenure_extend_timestamp();
12791307
if get_epoch_time_secs() <= tenure_extend_timestamp
12801308
&& self.tenure_change_time.elapsed() <= self.config.miner.tenure_timeout
@@ -1284,6 +1312,7 @@ impl BlockMinerThread {
12841312
tenure_change_tx: None,
12851313
});
12861314
}
1315+
12871316
info!("Miner: Time-based tenure extend";
12881317
"current_timestamp" => get_epoch_time_secs(),
12891318
"tenure_extend_timestamp" => tenure_extend_timestamp,

0 commit comments

Comments
 (0)