Skip to content

Commit a81469f

Browse files
authored
Merge pull request #5450 from stacks-network/feat/block-budget-spend-down
Spend down the block budget limit by x% every block
2 parents 20d5137 + 7a3946d commit a81469f

File tree

13 files changed

+544
-93
lines changed

13 files changed

+544
-93
lines changed

.github/workflows/bitcoin-tests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ jobs:
140140
- tests::nakamoto_integrations::utxo_check_on_startup_recover
141141
- tests::nakamoto_integrations::v3_signer_api_endpoint
142142
- tests::nakamoto_integrations::signer_chainstate
143+
- tests::nakamoto_integrations::clarity_cost_spend_down
143144
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
144145
# - tests::signer::v1::dkg
145146
# - tests::signer::v1::sign_request_rejected

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1212
- Remove the panic for reporting DB deadlocks (just error and continue waiting)
1313
- Add index to `metadata_table` in Clarity DB on `blockhash`
1414
- Add `block_commit_delay_ms` to the config file to control the time to wait after seeing a new burn block, before submitting a block commit, to allow time for the first Nakamoto block of the new tenure to be mined, allowing this miner to avoid the need to RBF the block commit.
15+
- Add `tenure_cost_limit_per_block_percentage` to the miner config file to control the percentage remaining tenure cost limit to consume per nakamoto block.
1516

1617
## [3.0.0.0.1]
1718

clarity/src/vm/costs/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,7 @@ impl LimitedCostTracker {
896896
Self::Free => ExecutionCost::max_value(),
897897
}
898898
}
899+
899900
pub fn get_memory(&self) -> u64 {
900901
match self {
901902
Self::Limited(TrackerData { memory, .. }) => *memory,
@@ -1170,6 +1171,7 @@ pub trait CostOverflowingMath<T> {
11701171
fn cost_overflow_mul(self, other: T) -> Result<T>;
11711172
fn cost_overflow_add(self, other: T) -> Result<T>;
11721173
fn cost_overflow_sub(self, other: T) -> Result<T>;
1174+
fn cost_overflow_div(self, other: T) -> Result<T>;
11731175
}
11741176

11751177
impl CostOverflowingMath<u64> for u64 {
@@ -1185,6 +1187,10 @@ impl CostOverflowingMath<u64> for u64 {
11851187
self.checked_sub(other)
11861188
.ok_or_else(|| CostErrors::CostOverflow)
11871189
}
1190+
fn cost_overflow_div(self, other: u64) -> Result<u64> {
1191+
self.checked_div(other)
1192+
.ok_or_else(|| CostErrors::CostOverflow)
1193+
}
11881194
}
11891195

11901196
impl ExecutionCost {
@@ -1293,6 +1299,15 @@ impl ExecutionCost {
12931299
Ok(())
12941300
}
12951301

1302+
pub fn divide(&mut self, divisor: u64) -> Result<()> {
1303+
self.runtime = self.runtime.cost_overflow_div(divisor)?;
1304+
self.read_count = self.read_count.cost_overflow_div(divisor)?;
1305+
self.read_length = self.read_length.cost_overflow_div(divisor)?;
1306+
self.write_length = self.write_length.cost_overflow_div(divisor)?;
1307+
self.write_count = self.write_count.cost_overflow_div(divisor)?;
1308+
Ok(())
1309+
}
1310+
12961311
/// Returns whether or not this cost exceeds any dimension of the
12971312
/// other cost.
12981313
pub fn exceeds(&self, other: &ExecutionCost) -> bool {

stackslib/src/chainstate/nakamoto/miner.rs

Lines changed: 119 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use clarity::vm::analysis::{CheckError, CheckErrors};
2525
use clarity::vm::ast::errors::ParseErrors;
2626
use clarity::vm::ast::ASTRules;
2727
use clarity::vm::clarity::TransactionConnection;
28-
use clarity::vm::costs::ExecutionCost;
28+
use clarity::vm::costs::{ExecutionCost, LimitedCostTracker, TrackerData};
2929
use clarity::vm::database::BurnStateDB;
3030
use clarity::vm::errors::Error as InterpreterError;
3131
use clarity::vm::types::{QualifiedContractIdentifier, TypeSignature};
@@ -124,6 +124,8 @@ pub struct NakamotoBlockBuilder {
124124
txs: Vec<StacksTransaction>,
125125
/// header we're filling in
126126
pub header: NakamotoBlockHeader,
127+
/// Optional soft limit for this block's budget usage
128+
soft_limit: Option<ExecutionCost>,
127129
}
128130

129131
pub struct MinerTenureInfo<'a> {
@@ -159,6 +161,7 @@ impl NakamotoBlockBuilder {
159161
bytes_so_far: 0,
160162
txs: vec![],
161163
header: NakamotoBlockHeader::genesis(),
164+
soft_limit: None,
162165
}
163166
}
164167

@@ -176,13 +179,18 @@ impl NakamotoBlockBuilder {
176179
///
177180
/// * `coinbase` - the coinbase tx if this is going to start a new tenure
178181
///
182+
/// * `bitvec_len` - the length of the bitvec of reward addresses that should be punished or not in this block.
183+
///
184+
/// * `soft_limit` - an optional soft limit for the block's clarity cost for this block
185+
///
179186
pub fn new(
180187
parent_stacks_header: &StacksHeaderInfo,
181188
tenure_id_consensus_hash: &ConsensusHash,
182189
total_burn: u64,
183190
tenure_change: Option<&StacksTransaction>,
184191
coinbase: Option<&StacksTransaction>,
185192
bitvec_len: u16,
193+
soft_limit: Option<ExecutionCost>,
186194
) -> Result<NakamotoBlockBuilder, Error> {
187195
let next_height = parent_stacks_header
188196
.anchored_header
@@ -222,6 +230,7 @@ impl NakamotoBlockBuilder {
222230
.map(|b| b.timestamp)
223231
.unwrap_or(0),
224232
),
233+
soft_limit,
225234
})
226235
}
227236

@@ -509,6 +518,7 @@ impl NakamotoBlockBuilder {
509518
tenure_info.tenure_change_tx(),
510519
tenure_info.coinbase_tx(),
511520
signer_bitvec_len,
521+
None,
512522
)?;
513523

514524
let ts_start = get_epoch_time_ms();
@@ -521,6 +531,37 @@ impl NakamotoBlockBuilder {
521531
.block_limit()
522532
.expect("Failed to obtain block limit from miner's block connection");
523533

534+
let mut soft_limit = None;
535+
if let Some(percentage) = settings
536+
.mempool_settings
537+
.tenure_cost_limit_per_block_percentage
538+
{
539+
// Make sure we aren't actually going to multiply by 0 or attempt to increase the block limit.
540+
assert!(
541+
(1..=100).contains(&percentage),
542+
"BUG: tenure_cost_limit_per_block_percentage: {percentage}%. Must be between between 1 and 100"
543+
);
544+
let mut remaining_limit = block_limit.clone();
545+
let cost_so_far = tenure_tx.cost_so_far();
546+
if remaining_limit.sub(&cost_so_far).is_ok() {
547+
if remaining_limit.divide(100).is_ok() {
548+
remaining_limit.multiply(percentage.into()).expect(
549+
"BUG: failed to multiply by {percentage} when previously divided by 100",
550+
);
551+
remaining_limit.add(&cost_so_far).expect("BUG: unexpected overflow when adding cost_so_far, which was previously checked");
552+
debug!(
553+
"Setting soft limit for clarity cost to {percentage}% of remaining block limit";
554+
"remaining_limit" => %remaining_limit,
555+
"cost_so_far" => %cost_so_far,
556+
"block_limit" => %block_limit,
557+
);
558+
soft_limit = Some(remaining_limit);
559+
}
560+
};
561+
}
562+
563+
builder.soft_limit = soft_limit;
564+
524565
let initial_txs: Vec<_> = [
525566
tenure_info.tenure_change_tx.clone(),
526567
tenure_info.coinbase_tx.clone(),
@@ -607,26 +648,19 @@ impl BlockBuilder for NakamotoBlockBuilder {
607648
return TransactionResult::skipped_due_to_error(&tx, Error::BlockTooBigError);
608649
}
609650

651+
let non_boot_code_contract_call = match &tx.payload {
652+
TransactionPayload::ContractCall(cc) => !cc.address.is_boot_code_addr(),
653+
TransactionPayload::SmartContract(..) => true,
654+
_ => false,
655+
};
656+
610657
match limit_behavior {
611658
BlockLimitFunction::CONTRACT_LIMIT_HIT => {
612-
match &tx.payload {
613-
TransactionPayload::ContractCall(cc) => {
614-
// once we've hit the runtime limit once, allow boot code contract calls, but do not try to eval
615-
// other contract calls
616-
if !cc.address.is_boot_code_addr() {
617-
return TransactionResult::skipped(
618-
&tx,
619-
"BlockLimitFunction::CONTRACT_LIMIT_HIT".to_string(),
620-
);
621-
}
622-
}
623-
TransactionPayload::SmartContract(..) => {
624-
return TransactionResult::skipped(
625-
&tx,
626-
"BlockLimitFunction::CONTRACT_LIMIT_HIT".to_string(),
627-
);
628-
}
629-
_ => {}
659+
if non_boot_code_contract_call {
660+
return TransactionResult::skipped(
661+
&tx,
662+
"BlockLimitFunction::CONTRACT_LIMIT_HIT".to_string(),
663+
);
630664
}
631665
}
632666
BlockLimitFunction::LIMIT_REACHED => {
@@ -653,70 +687,83 @@ impl BlockBuilder for NakamotoBlockBuilder {
653687
);
654688
return TransactionResult::problematic(&tx, Error::NetError(e));
655689
}
656-
let (fee, receipt) = match StacksChainState::process_transaction(
657-
clarity_tx, tx, quiet, ast_rules,
658-
) {
659-
Ok((fee, receipt)) => (fee, receipt),
660-
Err(e) => {
661-
let (is_problematic, e) =
662-
TransactionResult::is_problematic(&tx, e, clarity_tx.get_epoch());
663-
if is_problematic {
664-
return TransactionResult::problematic(&tx, e);
665-
} else {
666-
match e {
667-
Error::CostOverflowError(cost_before, cost_after, total_budget) => {
668-
clarity_tx.reset_cost(cost_before.clone());
669-
if total_budget.proportion_largest_dimension(&cost_before)
670-
< TX_BLOCK_LIMIT_PROPORTION_HEURISTIC
671-
{
672-
warn!(
673-
"Transaction {} consumed over {}% of block budget, marking as invalid; budget was {}",
674-
tx.txid(),
675-
100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC,
676-
&total_budget
677-
);
678-
let mut measured_cost = cost_after;
679-
let measured_cost = if measured_cost.sub(&cost_before).is_ok() {
680-
Some(measured_cost)
681-
} else {
682-
warn!(
683-
"Failed to compute measured cost of a too big transaction"
684-
);
685-
None
686-
};
687-
return TransactionResult::error(
688-
&tx,
689-
Error::TransactionTooBigError(measured_cost),
690-
);
691-
} else {
692-
warn!(
693-
"Transaction {} reached block cost {}; budget was {}",
694-
tx.txid(),
695-
&cost_after,
696-
&total_budget
697-
);
698-
return TransactionResult::skipped_due_to_error(
699-
&tx,
700-
Error::BlockTooBigError,
701-
);
702-
}
703-
}
704-
_ => return TransactionResult::error(&tx, e),
705-
}
690+
691+
let cost_before = clarity_tx.cost_so_far();
692+
let (fee, receipt) =
693+
match StacksChainState::process_transaction(clarity_tx, tx, quiet, ast_rules) {
694+
Ok(x) => x,
695+
Err(e) => {
696+
return parse_process_transaction_error(clarity_tx, tx, e);
706697
}
698+
};
699+
let cost_after = clarity_tx.cost_so_far();
700+
let mut soft_limit_reached = false;
701+
// We only attempt to apply the soft limit to non-boot code contract calls.
702+
if non_boot_code_contract_call {
703+
if let Some(soft_limit) = self.soft_limit.as_ref() {
704+
soft_limit_reached = cost_after.exceeds(soft_limit);
707705
}
708-
};
706+
}
707+
709708
info!("Include tx";
710709
"tx" => %tx.txid(),
711710
"payload" => tx.payload.name(),
712-
"origin" => %tx.origin_address());
711+
"origin" => %tx.origin_address(),
712+
"soft_limit_reached" => soft_limit_reached,
713+
"cost_after" => %cost_after,
714+
"cost_before" => %cost_before,
715+
);
713716

714717
// save
715718
self.txs.push(tx.clone());
716-
TransactionResult::success(&tx, fee, receipt)
719+
TransactionResult::success_with_soft_limit(&tx, fee, receipt, soft_limit_reached)
717720
};
718721

719722
self.bytes_so_far += tx_len;
720723
result
721724
}
722725
}
726+
727+
fn parse_process_transaction_error(
728+
clarity_tx: &mut ClarityTx,
729+
tx: &StacksTransaction,
730+
e: Error,
731+
) -> TransactionResult {
732+
let (is_problematic, e) = TransactionResult::is_problematic(&tx, e, clarity_tx.get_epoch());
733+
if is_problematic {
734+
TransactionResult::problematic(&tx, e)
735+
} else {
736+
match e {
737+
Error::CostOverflowError(cost_before, cost_after, total_budget) => {
738+
clarity_tx.reset_cost(cost_before.clone());
739+
if total_budget.proportion_largest_dimension(&cost_before)
740+
< TX_BLOCK_LIMIT_PROPORTION_HEURISTIC
741+
{
742+
warn!(
743+
"Transaction {} consumed over {}% of block budget, marking as invalid; budget was {}",
744+
tx.txid(),
745+
100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC,
746+
&total_budget
747+
);
748+
let mut measured_cost = cost_after;
749+
let measured_cost = if measured_cost.sub(&cost_before).is_ok() {
750+
Some(measured_cost)
751+
} else {
752+
warn!("Failed to compute measured cost of a too big transaction");
753+
None
754+
};
755+
TransactionResult::error(&tx, Error::TransactionTooBigError(measured_cost))
756+
} else {
757+
warn!(
758+
"Transaction {} reached block cost {}; budget was {}",
759+
tx.txid(),
760+
&cost_after,
761+
&total_budget
762+
);
763+
TransactionResult::skipped_due_to_error(&tx, Error::BlockTooBigError)
764+
}
765+
}
766+
_ => TransactionResult::error(&tx, e),
767+
}
768+
}
769+
}

stackslib/src/chainstate/nakamoto/tests/node.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,7 @@ impl TestStacksNode {
729729
None
730730
},
731731
1,
732+
None,
732733
)
733734
.unwrap()
734735
} else {

0 commit comments

Comments
 (0)