Skip to content

Commit 63a79aa

Browse files
committed
Merge branch 'develop' into feat/current-contract
2 parents 6c9a2f7 + d966da5 commit 63a79aa

File tree

12 files changed

+832
-60
lines changed

12 files changed

+832
-60
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
1616
- Adds support for new Clarity 4 builtins (not activated until epoch 3.3):
1717
- `contract-hash?`
1818
- `current-contract`
19+
- Added `contract_cost_limit_percentage` to the miner config file — sets the percentage of a block’s execution cost at which, if a large non-boot contract call would cause a BlockTooBigError, the miner will stop adding further non-boot contract calls and only include STX transfers and boot contract calls for the remainder of the block.
1920

2021
### Changed
2122

stacks-node/src/tests/nakamoto_integrations.rs

Lines changed: 682 additions & 5 deletions
Large diffs are not rendered by default.

stacks-node/src/tests/signer/v0.rs

Lines changed: 53 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3891,41 +3891,48 @@ fn tx_replay_btc_on_stx_invalidation() {
38913891
.expect("Timed out waiting for tx replay set to be updated");
38923892

38933893
info!("---- Waiting for tx replay set to be cleared ----");
3894-
3895-
let stacks_height_before = get_chain_info(&conf).stacks_tip_height;
3896-
3894+
test_observer::clear();
38973895
fault_injection_unstall_miner();
3898-
38993896
signer_test
39003897
.wait_for_signer_state_check(30, |state| Ok(state.get_tx_replay_set().is_none()))
39013898
.expect("Timed out waiting for tx replay set to be cleared");
39023899

3903-
// Ensure that only one block was mined
3904-
wait_for(30, || {
3905-
let new_tip = get_chain_info(&conf).stacks_tip_height;
3906-
Ok(new_tip == stacks_height_before + 1)
3907-
})
3908-
.expect("Timed out waiting for block to advance by 1");
3900+
let mut found_block = false;
3901+
// Ensure that we don't mine any of the replay transactions in a sufficient amount of elapsed time
3902+
let _ = wait_for(30, || {
3903+
let blocks = test_observer::get_blocks();
3904+
for block in blocks {
3905+
let block: StacksBlockEvent =
3906+
serde_json::from_value(block).expect("Failed to parse block");
3907+
for tx in block.transactions {
3908+
match tx.payload {
3909+
TransactionPayload::TenureChange(TenureChangePayload {
3910+
cause: TenureChangeCause::BlockFound,
3911+
..
3912+
})
3913+
| TransactionPayload::Coinbase(..) => {
3914+
found_block = true;
3915+
}
3916+
TransactionPayload::TenureChange(TenureChangePayload {
3917+
cause: TenureChangeCause::Extended,
3918+
..
3919+
}) => {
3920+
continue;
3921+
}
3922+
_ => {
3923+
panic!("We should not see any transactions mined beyond tenure change or coinbase txs");
3924+
}
3925+
}
3926+
}
3927+
}
3928+
Ok(false)
3929+
});
39093930

3931+
assert!(found_block, "Failed to mine the tenure change block");
3932+
// Ensure that in the 30 seconds, the nonce did not increase. This also asserts that no tx replays were mined.
39103933
let account = get_account(&_http_origin, &recipient_addr);
39113934
assert_eq!(account.nonce, 0, "Expected recipient nonce to be 0");
39123935

3913-
let blocks = test_observer::get_blocks();
3914-
let block: StacksBlockEvent =
3915-
serde_json::from_value(blocks.last().unwrap().clone()).expect("Failed to parse block");
3916-
assert_eq!(block.transactions.len(), 2);
3917-
assert!(matches!(
3918-
block.transactions[0].payload,
3919-
TransactionPayload::TenureChange(TenureChangePayload {
3920-
cause: TenureChangeCause::BlockFound,
3921-
..
3922-
})
3923-
));
3924-
assert!(matches!(
3925-
block.transactions[1].payload,
3926-
TransactionPayload::Coinbase(..)
3927-
));
3928-
39293936
signer_test.shutdown();
39303937
}
39313938

@@ -5466,7 +5473,7 @@ fn tx_replay_with_fork_middle_replay_while_tenure_extending_and_new_tx_submitted
54665473
signer_test
54675474
.wait_for_signer_state_check(60, |state| {
54685475
let tx_replay_set = state.get_tx_replay_set();
5469-
Ok(tx_replay_set.is_none())
5476+
Ok(tx_replay_set.is_none() && get_account(&http_origin, &sender1_addr).nonce >= 3)
54705477
})
54715478
.expect("Timed out waiting for tx replay set to be cleared");
54725479

@@ -6539,6 +6546,8 @@ fn tx_replay_budget_exceeded_tenure_extend() {
65396546

65406547
signer_test.wait_for_replay_set_eq(30, vec![txid1, txid2.clone()]);
65416548

6549+
// Clear the test observer so we know that if we see txid1 and txid2 again, that it means they were remined
6550+
test_observer::clear();
65426551
fault_injection_unstall_miner();
65436552

65446553
info!("---- Waiting for replay set to be cleared ----");
@@ -6547,25 +6556,25 @@ fn tx_replay_budget_exceeded_tenure_extend() {
65476556
signer_test
65486557
.wait_for_signer_state_check(30, |state| Ok(state.get_tx_replay_set().is_none()))
65496558
.expect("Timed out waiting for tx replay set to be cleared");
6550-
6551-
let blocks = test_observer::get_blocks();
65526559
let mut found_block: Option<StacksBlockEvent> = None;
6553-
// To reduce flakiness, we're just looking for the block containing `txid2`,
6554-
// which may or may not be the last block.
6555-
let last_blocks = blocks.iter().rev().take(3).collect::<Vec<_>>();
6556-
for block in last_blocks {
6557-
let block: StacksBlockEvent =
6558-
serde_json::from_value(block.clone()).expect("Failed to parse block");
6559-
if block
6560-
.transactions
6561-
.iter()
6562-
.find(|tx| tx.txid().to_hex() == txid2)
6563-
.is_some()
6564-
{
6565-
found_block = Some(block);
6566-
break;
6560+
wait_for(60, || {
6561+
let blocks = test_observer::get_blocks();
6562+
for block in blocks {
6563+
let block: StacksBlockEvent =
6564+
serde_json::from_value(block.clone()).expect("Failed to parse block");
6565+
if block
6566+
.transactions
6567+
.iter()
6568+
.find(|tx| tx.txid().to_hex() == txid2)
6569+
.is_some()
6570+
{
6571+
found_block = Some(block);
6572+
return Ok(true);
6573+
}
65676574
}
6568-
}
6575+
Ok(false)
6576+
})
6577+
.expect("Failed to mine the replay txs");
65696578
let block = found_block.expect("Failed to find block with txid2");
65706579
assert_eq!(block.transactions.len(), 2);
65716580
assert!(matches!(

stackslib/src/chainstate/nakamoto/miner.rs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use crate::chainstate::stacks::miner::{
3838
};
3939
use crate::chainstate::stacks::{Error, StacksBlockHeader, *};
4040
use crate::clarity_vm::clarity::ClarityInstance;
41+
use crate::config::DEFAULT_CONTRACT_COST_LIMIT_PERCENTAGE;
4142
use crate::core::mempool::*;
4243
use crate::core::*;
4344
use crate::monitoring::{
@@ -90,6 +91,9 @@ pub struct NakamotoBlockBuilder {
9091
pub header: NakamotoBlockHeader,
9192
/// Optional soft limit for this block's budget usage
9293
soft_limit: Option<ExecutionCost>,
94+
/// Percentage of a block's budget that may be consumed by
95+
/// contract calls before reverting to stx transfers/boot contract calls only
96+
contract_limit_percentage: Option<u8>,
9397
}
9498

9599
pub struct MinerTenureInfo<'a> {
@@ -141,6 +145,7 @@ impl NakamotoBlockBuilder {
141145
txs: vec![],
142146
header: NakamotoBlockHeader::genesis(),
143147
soft_limit: None,
148+
contract_limit_percentage: None,
144149
}
145150
}
146151

@@ -170,6 +175,7 @@ impl NakamotoBlockBuilder {
170175
coinbase: Option<&StacksTransaction>,
171176
bitvec_len: u16,
172177
soft_limit: Option<ExecutionCost>,
178+
contract_limit_percentage: Option<u8>,
173179
) -> Result<NakamotoBlockBuilder, Error> {
174180
let next_height = parent_stacks_header
175181
.anchored_header
@@ -210,6 +216,7 @@ impl NakamotoBlockBuilder {
210216
.unwrap_or(0),
211217
),
212218
soft_limit,
219+
contract_limit_percentage,
213220
})
214221
}
215222

@@ -534,6 +541,7 @@ impl NakamotoBlockBuilder {
534541
tenure_info.coinbase_tx(),
535542
signer_bitvec_len,
536543
None,
544+
settings.mempool_settings.contract_cost_limit_percentage,
537545
)?;
538546

539547
let ts_start = get_epoch_time_ms();
@@ -712,7 +720,6 @@ impl BlockBuilder for NakamotoBlockBuilder {
712720
}
713721

714722
let cost_before = clarity_tx.cost_so_far();
715-
716723
let (_fee, receipt) = match StacksChainState::process_transaction(
717724
clarity_tx,
718725
tx,
@@ -722,7 +729,13 @@ impl BlockBuilder for NakamotoBlockBuilder {
722729
) {
723730
Ok(x) => x,
724731
Err(e) => {
725-
return parse_process_transaction_error(clarity_tx, tx, e);
732+
return parse_process_transaction_error(
733+
clarity_tx,
734+
tx,
735+
e,
736+
self.contract_limit_percentage
737+
.unwrap_or(DEFAULT_CONTRACT_COST_LIMIT_PERCENTAGE),
738+
);
726739
}
727740
};
728741

@@ -741,7 +754,7 @@ impl BlockBuilder for NakamotoBlockBuilder {
741754
"origin" => %tx.origin_address(),
742755
"soft_limit_reached" => soft_limit_reached,
743756
"cost_after" => %cost_after,
744-
"cost_before" => %cost_before,
757+
"cost_before" => %cost_before
745758
);
746759

747760
// save
@@ -758,6 +771,7 @@ fn parse_process_transaction_error(
758771
clarity_tx: &mut ClarityTx,
759772
tx: &StacksTransaction,
760773
e: Error,
774+
contract_limit_percentage: u8,
761775
) -> TransactionResult {
762776
let (is_problematic, e) = TransactionResult::is_problematic(tx, e, clarity_tx.get_epoch());
763777
if is_problematic {
@@ -766,14 +780,13 @@ fn parse_process_transaction_error(
766780
match e {
767781
Error::CostOverflowError(cost_before, cost_after, total_budget) => {
768782
clarity_tx.reset_cost(cost_before.clone());
769-
if total_budget.proportion_largest_dimension(&cost_before)
770-
< TX_BLOCK_LIMIT_PROPORTION_HEURISTIC
771-
{
783+
let cost_so_far_percentage =
784+
total_budget.proportion_largest_dimension(&cost_before);
785+
if cost_so_far_percentage < TX_BLOCK_LIMIT_PROPORTION_HEURISTIC {
772786
warn!(
773-
"Transaction {} consumed over {}% of block budget, marking as invalid; budget was {}",
787+
"Transaction {} consumed over {}% of block budget, marking as invalid; budget was {total_budget}",
774788
tx.txid(),
775-
100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC,
776-
&total_budget
789+
100 - TX_BLOCK_LIMIT_PROPORTION_HEURISTIC
777790
);
778791
let mut measured_cost = cost_after;
779792
let measured_cost = if measured_cost.sub(&cost_before).is_ok() {
@@ -783,6 +796,13 @@ fn parse_process_transaction_error(
783796
None
784797
};
785798
TransactionResult::error(tx, Error::TransactionTooBigError(measured_cost))
799+
} else if cost_so_far_percentage < contract_limit_percentage.into() {
800+
warn!(
801+
"Transaction {} would exceed the tenure budget, but only {cost_so_far_percentage}% of total budget currently consumed. Skipping tx for this block.", tx.txid();
802+
"contract_limit_percentage" => contract_limit_percentage,
803+
"total_budget" => %total_budget
804+
);
805+
TransactionResult::skipped_due_to_error(tx, Error::BlockCostLimitError)
786806
} else {
787807
warn!(
788808
"Transaction {} reached block cost {cost_after}; budget was {total_budget}",

stackslib/src/chainstate/nakamoto/shadow.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,7 @@ impl NakamotoBlockBuilder {
727727
Some(&coinbase_tx),
728728
1,
729729
None,
730+
None,
730731
)?;
731732

732733
let mut block_txs = vec![tenure_change_tx, coinbase_tx];

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ impl TestStacksNode {
796796
},
797797
1,
798798
None,
799+
None,
799800
)?
800801
} else {
801802
NakamotoBlockBuilder::new_first_block(

stackslib/src/chainstate/stacks/miner.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2879,9 +2879,10 @@ fn select_and_apply_transactions_from_vec<B: BlockBuilder>(
28792879
TransactionResult::Skipped(TransactionSkipped { error, .. })
28802880
| TransactionResult::ProcessingError(TransactionError { error, .. }) => {
28812881
match &error {
2882-
Error::BlockTooBigError => {
2882+
Error::BlockTooBigError | Error::BlockCostLimitError => {
28832883
// done mining -- our execution budget is exceeded.
2884-
// Make the block from the transactions we did manage to get
2884+
// Make the block from the transactions we did manage
2885+
// (We cannot simply skip as this would put the replay txs out of order)
28852886
debug!("Block budget exceeded on tx {txid}");
28862887
info!("Miner stopping due to limit reached");
28872888
break;

stackslib/src/chainstate/stacks/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ pub enum Error {
9292
NotInSameFork,
9393
InvalidChainstateDB,
9494
BlockTooBigError,
95+
BlockCostLimitError,
9596
TransactionTooBigError(Option<ExecutionCost>),
9697
BlockCostExceeded,
9798
NoTransactionsToMine,
@@ -156,6 +157,7 @@ impl fmt::Display for Error {
156157
Error::NoSuchBlockError => write!(f, "No such Stacks block"),
157158
Error::InvalidChainstateDB => write!(f, "Invalid chainstate database"),
158159
Error::BlockTooBigError => write!(f, "Too much data in block"),
160+
Error::BlockCostLimitError => write!(f, "Block cost limit exceeded"),
159161
Error::TransactionTooBigError(ref c) => {
160162
write!(f, "Too much data in transaction: measured_cost={c:?}")
161163
}
@@ -236,6 +238,7 @@ impl error::Error for Error {
236238
Error::NoSuchBlockError => None,
237239
Error::InvalidChainstateDB => None,
238240
Error::BlockTooBigError => None,
241+
Error::BlockCostLimitError => None,
239242
Error::TransactionTooBigError(..) => None,
240243
Error::BlockCostExceeded => None,
241244
Error::MicroblockStreamTooLongError => None,
@@ -281,6 +284,7 @@ impl Error {
281284
Error::NoSuchBlockError => "NoSuchBlockError",
282285
Error::InvalidChainstateDB => "InvalidChainstateDB",
283286
Error::BlockTooBigError => "BlockTooBigError",
287+
Error::BlockCostLimitError => "BlockCostLimitError",
284288
Error::TransactionTooBigError(..) => "TransactionTooBigError",
285289
Error::BlockCostExceeded => "BlockCostExceeded",
286290
Error::MicroblockStreamTooLongError => "MicroblockStreamTooLongError",

0 commit comments

Comments
 (0)