Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spend down the block budget limit by x% every block #5450

Open
wants to merge 30 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1443e8c
Spend down the block budget limit by 25% every block
jferrant Nov 11, 2024
74da2c8
Add tenure_cost_limit_per_block_percentage config option and only app…
jferrant Nov 11, 2024
33091d7
Convert cost_limit_percentage to a soft limit
jferrant Nov 12, 2024
ef332d2
Use the block limit BEFORE applying the hard check when running the s…
jferrant Nov 12, 2024
ef4426b
Do not apply the spend down per transaction rather do per block
jferrant Nov 12, 2024
068d18f
Fix build
jferrant Nov 12, 2024
7dbd45c
Fix tests to not spend down the tenure limit
jferrant Nov 12, 2024
c9c950d
Fix comment
jferrant Nov 12, 2024
fbf5564
WIP: incomplete test
jferrant Nov 13, 2024
c7f65f8
WIP: incomplete test
jferrant Nov 13, 2024
33ec9b8
WIP: incomplete test
jferrant Nov 13, 2024
728c5f0
Fix soft limit calculation
jferrant Nov 13, 2024
f15b426
Fix setting of soft limit
jferrant Nov 13, 2024
5f7d661
wip: use contract-call and change soft_limit_reached logic
hstove Nov 14, 2024
edb6429
Cleanup
jferrant Nov 14, 2024
2803411
fix: proper `soft_limit` set, and fix test mined_blocks assertion
hstove Nov 14, 2024
3452aa7
Cleanup
jferrant Nov 14, 2024
0c46dbe
WIP: incomplete test
jferrant Nov 14, 2024
d78bf72
Make test a bit stricter about soft limit check
jferrant Nov 14, 2024
9eb5a80
Fix failing tests
jferrant Nov 14, 2024
1e0fbba
CRC: use cost_after to determine if soft limit was exceeded
jferrant Nov 14, 2024
975251b
CRC: cleanup comment for soft_limit option
jferrant Nov 14, 2024
56087b0
CRC: fix miner config parsing
jferrant Nov 14, 2024
804b8b9
Remove old log
jferrant Nov 14, 2024
21ed874
Use ranges when checking percentage
jferrant Nov 14, 2024
95ca231
Default to 25% for tenure_cost_limit_per_block_percentage
jferrant Nov 15, 2024
f27e918
CRC: cleanup
jferrant Nov 15, 2024
99d9a80
Add an assert to account for a faulty test
jferrant Nov 15, 2024
b2d6fad
Cleanup
jferrant Nov 15, 2024
7a3946d
Fix assert
jferrant Nov 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/bitcoin-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,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;
hstove marked this conversation as resolved.
Show resolved Hide resolved

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;
jferrant marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading