Skip to content

Commit

Permalink
Merge pull request #5450 from stacks-network/feat/block-budget-spend-…
Browse files Browse the repository at this point in the history
…down

Spend down the block budget limit by x% every block
  • Loading branch information
jferrant authored Nov 19, 2024
2 parents 20d5137 + 7a3946d commit a81469f
Show file tree
Hide file tree
Showing 13 changed files with 544 additions and 93 deletions.
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ jobs:
- tests::nakamoto_integrations::utxo_check_on_startup_recover
- tests::nakamoto_integrations::v3_signer_api_endpoint
- tests::nakamoto_integrations::signer_chainstate
- tests::nakamoto_integrations::clarity_cost_spend_down
# TODO: enable these once v1 signer is supported by a new nakamoto epoch
# - tests::signer::v1::dkg
# - tests::signer::v1::sign_request_rejected
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
- Remove the panic for reporting DB deadlocks (just error and continue waiting)
- Add index to `metadata_table` in Clarity DB on `blockhash`
- 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.
- 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.

## [3.0.0.0.1]

Expand Down
15 changes: 15 additions & 0 deletions clarity/src/vm/costs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,7 @@ impl LimitedCostTracker {
Self::Free => ExecutionCost::max_value(),
}
}

pub fn get_memory(&self) -> u64 {
match self {
Self::Limited(TrackerData { memory, .. }) => *memory,
Expand Down Expand Up @@ -1170,6 +1171,7 @@ pub trait CostOverflowingMath<T> {
fn cost_overflow_mul(self, other: T) -> Result<T>;
fn cost_overflow_add(self, other: T) -> Result<T>;
fn cost_overflow_sub(self, other: T) -> Result<T>;
fn cost_overflow_div(self, other: T) -> Result<T>;
}

impl CostOverflowingMath<u64> for u64 {
Expand All @@ -1185,6 +1187,10 @@ impl CostOverflowingMath<u64> for u64 {
self.checked_sub(other)
.ok_or_else(|| CostErrors::CostOverflow)
}
fn cost_overflow_div(self, other: u64) -> Result<u64> {
self.checked_div(other)
.ok_or_else(|| CostErrors::CostOverflow)
}
}

impl ExecutionCost {
Expand Down Expand Up @@ -1293,6 +1299,15 @@ impl ExecutionCost {
Ok(())
}

pub fn divide(&mut self, divisor: u64) -> Result<()> {
self.runtime = self.runtime.cost_overflow_div(divisor)?;
self.read_count = self.read_count.cost_overflow_div(divisor)?;
self.read_length = self.read_length.cost_overflow_div(divisor)?;
self.write_length = self.write_length.cost_overflow_div(divisor)?;
self.write_count = self.write_count.cost_overflow_div(divisor)?;
Ok(())
}

/// Returns whether or not this cost exceeds any dimension of the
/// other cost.
pub fn exceeds(&self, other: &ExecutionCost) -> bool {
Expand Down
191 changes: 119 additions & 72 deletions stackslib/src/chainstate/nakamoto/miner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use clarity::vm::analysis::{CheckError, CheckErrors};
use clarity::vm::ast::errors::ParseErrors;
use clarity::vm::ast::ASTRules;
use clarity::vm::clarity::TransactionConnection;
use clarity::vm::costs::ExecutionCost;
use clarity::vm::costs::{ExecutionCost, LimitedCostTracker, TrackerData};
use clarity::vm::database::BurnStateDB;
use clarity::vm::errors::Error as InterpreterError;
use clarity::vm::types::{QualifiedContractIdentifier, TypeSignature};
Expand Down Expand Up @@ -124,6 +124,8 @@ pub struct NakamotoBlockBuilder {
txs: Vec<StacksTransaction>,
/// header we're filling in
pub header: NakamotoBlockHeader,
/// Optional soft limit for this block's budget usage
soft_limit: Option<ExecutionCost>,
}

pub struct MinerTenureInfo<'a> {
Expand Down Expand Up @@ -159,6 +161,7 @@ impl NakamotoBlockBuilder {
bytes_so_far: 0,
txs: vec![],
header: NakamotoBlockHeader::genesis(),
soft_limit: None,
}
}

Expand All @@ -176,13 +179,18 @@ impl NakamotoBlockBuilder {
///
/// * `coinbase` - the coinbase tx if this is going to start a new tenure
///
/// * `bitvec_len` - the length of the bitvec of reward addresses that should be punished or not in this block.
///
/// * `soft_limit` - an optional soft limit for the block's clarity cost for this block
///
pub fn new(
parent_stacks_header: &StacksHeaderInfo,
tenure_id_consensus_hash: &ConsensusHash,
total_burn: u64,
tenure_change: Option<&StacksTransaction>,
coinbase: Option<&StacksTransaction>,
bitvec_len: u16,
soft_limit: Option<ExecutionCost>,
) -> Result<NakamotoBlockBuilder, Error> {
let next_height = parent_stacks_header
.anchored_header
Expand Down Expand Up @@ -222,6 +230,7 @@ impl NakamotoBlockBuilder {
.map(|b| b.timestamp)
.unwrap_or(0),
),
soft_limit,
})
}

Expand Down Expand Up @@ -509,6 +518,7 @@ impl NakamotoBlockBuilder {
tenure_info.tenure_change_tx(),
tenure_info.coinbase_tx(),
signer_bitvec_len,
None,
)?;

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

let mut soft_limit = None;
if let Some(percentage) = settings
.mempool_settings
.tenure_cost_limit_per_block_percentage
{
// Make sure we aren't actually going to multiply by 0 or attempt to increase the block limit.
assert!(
(1..=100).contains(&percentage),
"BUG: tenure_cost_limit_per_block_percentage: {percentage}%. Must be between between 1 and 100"
);
let mut remaining_limit = block_limit.clone();
let cost_so_far = tenure_tx.cost_so_far();
if remaining_limit.sub(&cost_so_far).is_ok() {
if remaining_limit.divide(100).is_ok() {
remaining_limit.multiply(percentage.into()).expect(
"BUG: failed to multiply by {percentage} when previously divided by 100",
);
remaining_limit.add(&cost_so_far).expect("BUG: unexpected overflow when adding cost_so_far, which was previously checked");
debug!(
"Setting soft limit for clarity cost to {percentage}% of remaining block limit";
"remaining_limit" => %remaining_limit,
"cost_so_far" => %cost_so_far,
"block_limit" => %block_limit,
);
soft_limit = Some(remaining_limit);
}
};
}

builder.soft_limit = soft_limit;

let initial_txs: Vec<_> = [
tenure_info.tenure_change_tx.clone(),
tenure_info.coinbase_tx.clone(),
Expand Down Expand Up @@ -607,26 +648,19 @@ impl BlockBuilder for NakamotoBlockBuilder {
return TransactionResult::skipped_due_to_error(&tx, Error::BlockTooBigError);
}

let non_boot_code_contract_call = match &tx.payload {
TransactionPayload::ContractCall(cc) => !cc.address.is_boot_code_addr(),
TransactionPayload::SmartContract(..) => true,
_ => false,
};

match limit_behavior {
BlockLimitFunction::CONTRACT_LIMIT_HIT => {
match &tx.payload {
TransactionPayload::ContractCall(cc) => {
// once we've hit the runtime limit once, allow boot code contract calls, but do not try to eval
// other contract calls
if !cc.address.is_boot_code_addr() {
return TransactionResult::skipped(
&tx,
"BlockLimitFunction::CONTRACT_LIMIT_HIT".to_string(),
);
}
}
TransactionPayload::SmartContract(..) => {
return TransactionResult::skipped(
&tx,
"BlockLimitFunction::CONTRACT_LIMIT_HIT".to_string(),
);
}
_ => {}
if non_boot_code_contract_call {
return TransactionResult::skipped(
&tx,
"BlockLimitFunction::CONTRACT_LIMIT_HIT".to_string(),
);
}
}
BlockLimitFunction::LIMIT_REACHED => {
Expand All @@ -653,70 +687,83 @@ impl BlockBuilder for NakamotoBlockBuilder {
);
return TransactionResult::problematic(&tx, Error::NetError(e));
}
let (fee, receipt) = match StacksChainState::process_transaction(
clarity_tx, tx, quiet, ast_rules,
) {
Ok((fee, receipt)) => (fee, receipt),
Err(e) => {
let (is_problematic, e) =
TransactionResult::is_problematic(&tx, e, clarity_tx.get_epoch());
if is_problematic {
return TransactionResult::problematic(&tx, e);
} else {
match e {
Error::CostOverflowError(cost_before, cost_after, total_budget) => {
clarity_tx.reset_cost(cost_before.clone());
if total_budget.proportion_largest_dimension(&cost_before)
< TX_BLOCK_LIMIT_PROPORTION_HEURISTIC
{
warn!(
"Transaction {} consumed over {}% of block budget, marking as invalid; budget was {}",
tx.txid(),
100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC,
&total_budget
);
let mut measured_cost = cost_after;
let measured_cost = if measured_cost.sub(&cost_before).is_ok() {
Some(measured_cost)
} else {
warn!(
"Failed to compute measured cost of a too big transaction"
);
None
};
return TransactionResult::error(
&tx,
Error::TransactionTooBigError(measured_cost),
);
} else {
warn!(
"Transaction {} reached block cost {}; budget was {}",
tx.txid(),
&cost_after,
&total_budget
);
return TransactionResult::skipped_due_to_error(
&tx,
Error::BlockTooBigError,
);
}
}
_ => return TransactionResult::error(&tx, e),
}

let cost_before = clarity_tx.cost_so_far();
let (fee, receipt) =
match StacksChainState::process_transaction(clarity_tx, tx, quiet, ast_rules) {
Ok(x) => x,
Err(e) => {
return parse_process_transaction_error(clarity_tx, tx, e);
}
};
let cost_after = clarity_tx.cost_so_far();
let mut soft_limit_reached = false;
// We only attempt to apply the soft limit to non-boot code contract calls.
if non_boot_code_contract_call {
if let Some(soft_limit) = self.soft_limit.as_ref() {
soft_limit_reached = cost_after.exceeds(soft_limit);
}
};
}

info!("Include tx";
"tx" => %tx.txid(),
"payload" => tx.payload.name(),
"origin" => %tx.origin_address());
"origin" => %tx.origin_address(),
"soft_limit_reached" => soft_limit_reached,
"cost_after" => %cost_after,
"cost_before" => %cost_before,
);

// save
self.txs.push(tx.clone());
TransactionResult::success(&tx, fee, receipt)
TransactionResult::success_with_soft_limit(&tx, fee, receipt, soft_limit_reached)
};

self.bytes_so_far += tx_len;
result
}
}

fn parse_process_transaction_error(
clarity_tx: &mut ClarityTx,
tx: &StacksTransaction,
e: Error,
) -> TransactionResult {
let (is_problematic, e) = TransactionResult::is_problematic(&tx, e, clarity_tx.get_epoch());
if is_problematic {
TransactionResult::problematic(&tx, e)
} else {
match e {
Error::CostOverflowError(cost_before, cost_after, total_budget) => {
clarity_tx.reset_cost(cost_before.clone());
if total_budget.proportion_largest_dimension(&cost_before)
< TX_BLOCK_LIMIT_PROPORTION_HEURISTIC
{
warn!(
"Transaction {} consumed over {}% of block budget, marking as invalid; budget was {}",
tx.txid(),
100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC,
&total_budget
);
let mut measured_cost = cost_after;
let measured_cost = if measured_cost.sub(&cost_before).is_ok() {
Some(measured_cost)
} else {
warn!("Failed to compute measured cost of a too big transaction");
None
};
TransactionResult::error(&tx, Error::TransactionTooBigError(measured_cost))
} else {
warn!(
"Transaction {} reached block cost {}; budget was {}",
tx.txid(),
&cost_after,
&total_budget
);
TransactionResult::skipped_due_to_error(&tx, Error::BlockTooBigError)
}
}
_ => TransactionResult::error(&tx, e),
}
}
}
1 change: 1 addition & 0 deletions stackslib/src/chainstate/nakamoto/tests/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,7 @@ impl TestStacksNode {
None
},
1,
None,
)
.unwrap()
} else {
Expand Down
Loading

0 comments on commit a81469f

Please sign in to comment.