Skip to content

Commit 8803ff1

Browse files
committed
feat: do not issue a time-based tenure extend earlier than needed
With this change, a miner will not issue a time-based tenure extend if it has used less than X% of the tenure budget, where X can be specified in the `tenure_extend_cost_threshold` config option.
1 parent 2233e5e commit 8803ff1

File tree

5 files changed

+99
-31
lines changed

5 files changed

+99
-31
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Miner config 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
13+
1014
### Changed
1115

1216
- 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)
1318

1419
## [3.1.0.0.4]
1520

stackslib/src/chainstate/nakamoto/miner.rs

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

153+
pub struct BlockMetadata {
154+
pub block: NakamotoBlock,
155+
pub tenure_consumed: ExecutionCost,
156+
pub tenure_budget: ExecutionCost,
157+
pub tenure_size: u64,
158+
pub tx_events: Vec<TransactionEvent>,
159+
}
160+
153161
impl NakamotoBlockBuilder {
154162
/// Make a block builder from genesis (testing only)
155163
pub fn new_first_block(
@@ -526,7 +534,7 @@ impl NakamotoBlockBuilder {
526534
settings: BlockBuilderSettings,
527535
event_observer: Option<&dyn MemPoolEventDispatcher>,
528536
signer_bitvec_len: u16,
529-
) -> Result<(NakamotoBlock, ExecutionCost, u64, Vec<TransactionEvent>), Error> {
537+
) -> Result<BlockMetadata, Error> {
530538
let (tip_consensus_hash, tip_block_hash, tip_height) = (
531539
parent_stacks_header.consensus_hash.clone(),
532540
parent_stacks_header.anchored_header.block_hash(),
@@ -556,7 +564,7 @@ impl NakamotoBlockBuilder {
556564
builder.load_tenure_info(&mut chainstate, burn_dbconn, tenure_info.cause())?;
557565
let mut tenure_tx = builder.tenure_begin(burn_dbconn, &mut miner_tenure_info)?;
558566

559-
let block_limit = tenure_tx
567+
let tenure_budget = tenure_tx
560568
.block_limit()
561569
.expect("Failed to obtain block limit from miner's block connection");
562570

@@ -570,7 +578,7 @@ impl NakamotoBlockBuilder {
570578
(1..=100).contains(&percentage),
571579
"BUG: tenure_cost_limit_per_block_percentage: {percentage}%. Must be between between 1 and 100"
572580
);
573-
let mut remaining_limit = block_limit.clone();
581+
let mut remaining_limit = tenure_budget.clone();
574582
let cost_so_far = tenure_tx.cost_so_far();
575583
if remaining_limit.sub(&cost_so_far).is_ok() && remaining_limit.divide(100).is_ok() {
576584
remaining_limit.multiply(percentage.into()).expect(
@@ -581,7 +589,7 @@ impl NakamotoBlockBuilder {
581589
"Setting soft limit for clarity cost to {percentage}% of remaining block limit";
582590
"remaining_limit" => %remaining_limit,
583591
"cost_so_far" => %cost_so_far,
584-
"block_limit" => %block_limit,
592+
"block_limit" => %tenure_budget,
585593
);
586594
soft_limit = Some(remaining_limit);
587595
};
@@ -630,13 +638,13 @@ impl NakamotoBlockBuilder {
630638

631639
// save the block so we can build microblocks off of it
632640
let block = builder.mine_nakamoto_block(&mut tenure_tx);
633-
let size = builder.bytes_so_far;
634-
let consumed = builder.tenure_finish(tenure_tx)?;
641+
let tenure_size = builder.bytes_so_far;
642+
let tenure_consumed = builder.tenure_finish(tenure_tx)?;
635643

636644
let ts_end = get_epoch_time_ms();
637645

638646
set_last_mined_block_transaction_count(block.txs.len() as u64);
639-
set_last_mined_execution_cost_observed(&consumed, &block_limit);
647+
set_last_mined_execution_cost_observed(&tenure_consumed, &tenure_budget);
640648

641649
info!(
642650
"Miner: mined Nakamoto block";
@@ -645,14 +653,20 @@ impl NakamotoBlockBuilder {
645653
"height" => block.header.chain_length,
646654
"tx_count" => block.txs.len(),
647655
"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),
656+
"block_size" => tenure_size,
657+
"execution_consumed" => %tenure_consumed,
658+
"percent_full" => tenure_budget.proportion_largest_dimension(&tenure_consumed),
651659
"assembly_time_ms" => ts_end.saturating_sub(ts_start),
652660
"consensus_hash" => %block.header.consensus_hash
653661
);
654662

655-
Ok((block, consumed, size, tx_events))
663+
Ok(BlockMetadata {
664+
block,
665+
tenure_consumed,
666+
tenure_budget,
667+
tenure_size,
668+
tx_events,
669+
})
656670
}
657671

658672
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: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ const DEFAULT_TENURE_COST_LIMIT_PER_BLOCK_PERCENTAGE: u8 = 25;
9797
const DEFAULT_TENURE_EXTEND_POLL_SECS: u64 = 1;
9898

9999
// This should be greater than the signers' timeout. This is used for issuing fallback tenure extends
100-
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 420;
100+
const DEFAULT_TENURE_TIMEOUT_SECS: u64 = 180;
101+
const DEFAULT_TENURE_EXTEND_COST_THRESHOLD: u64 = 50;
101102

102103
static HELIUM_DEFAULT_CONNECTION_OPTIONS: LazyLock<ConnectionOptions> =
103104
LazyLock::new(|| ConnectionOptions {
@@ -2155,6 +2156,8 @@ pub struct MinerConfig {
21552156
pub tenure_extend_poll_secs: Duration,
21562157
/// Duration to wait before attempting to issue a tenure extend
21572158
pub tenure_timeout: Duration,
2159+
/// Percentage of block budget that must be used before attempting a time-based tenure extend
2160+
pub tenure_extend_cost_threshold: u64,
21582161
}
21592162

21602163
impl Default for MinerConfig {
@@ -2193,6 +2196,7 @@ impl Default for MinerConfig {
21932196
),
21942197
tenure_extend_poll_secs: Duration::from_secs(DEFAULT_TENURE_EXTEND_POLL_SECS),
21952198
tenure_timeout: Duration::from_secs(DEFAULT_TENURE_TIMEOUT_SECS),
2199+
tenure_extend_cost_threshold: DEFAULT_TENURE_EXTEND_COST_THRESHOLD,
21962200
}
21972201
}
21982202
}
@@ -2589,6 +2593,7 @@ pub struct MinerConfigFile {
25892593
pub tenure_cost_limit_per_block_percentage: Option<u8>,
25902594
pub tenure_extend_poll_secs: Option<u64>,
25912595
pub tenure_timeout_secs: Option<u64>,
2596+
pub tenure_extend_cost_threshold: Option<u64>,
25922597
}
25932598

25942599
impl MinerConfigFile {
@@ -2731,6 +2736,7 @@ impl MinerConfigFile {
27312736
tenure_cost_limit_per_block_percentage,
27322737
tenure_extend_poll_secs: self.tenure_extend_poll_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_extend_poll_secs),
27332738
tenure_timeout: self.tenure_timeout_secs.map(Duration::from_secs).unwrap_or(miner_default_config.tenure_timeout),
2739+
tenure_extend_cost_threshold: self.tenure_extend_cost_threshold.unwrap_or(miner_default_config.tenure_extend_cost_threshold),
27342740
})
27352741
}
27362742
}

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)