Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
255 changes: 251 additions & 4 deletions stackslib/src/chainstate/tests/consensus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use clarity::util::hash::{MerkleTree, Sha512Trunc256Sum};
use clarity::util::secp256k1::MessageSignature;
use clarity::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER;
use clarity::vm::costs::ExecutionCost;
use clarity::vm::types::PrincipalData;
use clarity::vm::types::{OptionalData, PrincipalData, ResponseData};
use clarity::vm::{ClarityVersion, Value as ClarityValue, MAX_CALL_STACK_DEPTH};
use serde::{Deserialize, Serialize, Serializer};
use stacks_common::bitvec::BitVec;
Expand Down Expand Up @@ -1163,9 +1163,256 @@ contract_deploy_consensus_test!(
chainstate_error_expression_stack_depth_too_deep,
contract_name: "test-exceeds",
contract_code: &{
let exceeds_repeat_factor = AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64);
let tx_exceeds_body_start = "{ a : ".repeat(exceeds_repeat_factor as usize);
let tx_exceeds_body_end = "} ".repeat(exceeds_repeat_factor as usize);
let exceeds_repeat_factor = AST_CALL_STACK_DEPTH_BUFFER + (MAX_CALL_STACK_DEPTH as u64);
let tx_exceeds_body_start = "{ a : ".repeat(exceeds_repeat_factor as usize);
let tx_exceeds_body_end = "} ".repeat(exceeds_repeat_factor as usize);
format!("{tx_exceeds_body_start}u1 {tx_exceeds_body_end}")
},
);

// Tests that MemoryBalanceExceeded error during contract analysis invalidates the block.
// The entire block will be rejected.
//
// This test creates a contract that fails during analysis phase.
// The contract defines large nested tuple constants that exhaust
// the 100MB memory limit during analysis.
contract_deploy_consensus_test!(
chainstate_error_memory_balance_exceeded_during_contract_analysis,
contract_name: "analysis-memory-test",
contract_code: &{
let mut contract = String::new();
// size: t0: 36 bytes
contract.push_str("(define-constant t0 (tuple (f0 0x00) (f1 0x00) (f2 0x00) (f3 0x00)))");
// size: t1: 160 bytes
contract.push_str("(define-constant t1 (tuple (f0 t0) (f1 t0) (f2 t0) (f3 t0)))");
// size: t2: 656 bytes
contract.push_str("(define-constant t2 (tuple (f0 t1) (f1 t1) (f2 t1) (f3 t1)))");
// size: t3: 2640 bytes
contract.push_str("(define-constant t3 (tuple (f0 t2) (f1 t2) (f2 t2) (f3 t2)))");
// size: t4: 10576 bytes
contract.push_str("(define-constant t4 (tuple (f0 t3) (f1 t3) (f2 t3) (f3 t3)))");
// size: t5: 42320 bytes
contract.push_str("(define-constant t5 (tuple (f0 t4) (f1 t4) (f2 t4) (f3 t4)))");
// size: t6: 126972 bytes
contract.push_str("(define-constant t6 (tuple (f0 t5) (f1 t5) (f2 t5)))");
// 126972 bytes * 800 ~= 101577600. Triggers MemoryBalanceExceeded during analysis.
for i in 0..800 {
contract.push_str(&format!("(define-constant l{} t6)", i + 1));
}
contract
},
);

// Tests that `MemoryBalanceExceeded` error occurs during contract initialization.
// The transaction will fail but will still be committed to the block.
//
// This test creates a contract that successfully passes analysis but fails during initialization
// The contract defines large buffer constants (buff-20 = 1MB) and then creates many references
// to it in a top-level `is-eq` expression, which exhausts the 100MB memory limit during initialization.
contract_deploy_consensus_test!(
chainstate_error_memory_balance_exceeded_during_contract_initialization,
contract_name: "test-exceeds",
contract_code: &{
let define_data_var = "(define-constant buff-0 0x00)";

let mut contract = define_data_var.to_string();
for i in 0..20 {
contract.push('\n');
contract.push_str(&format!(
"(define-constant buff-{} (concat buff-{i} buff-{i}))",
i + 1,
));
}

contract.push('\n');
contract.push_str("(is-eq ");

for _i in 0..100 {
let exploder = "buff-20 ";
contract.push_str(exploder);
}

contract.push(')');
contract
},
);

// Tests that `MemoryBalanceExceeded` error occurs during a contract call.
// The transaction will fail but will still be committed to the block.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. Should remove this empty line.

// This test creates a contract that successfully passes analysis and initialization
// but fails during the call phase when the `create-many-references` function is called.
// It creates many references to a large buffer (buff-20 = 1MB) in an `is-eq`
// expression, which exhausts the 100MB memory limit during function execution.
contract_call_consensus_test!(
chainstate_error_memory_balance_exceeded_during_contract_call,
contract_name: "memory-test-contract",
contract_code: &{
// Procedurally generate a contract with large buffer constants and a function
// that creates many references to them, similar to argument_memory_test
let mut contract = String::new();

// Create buff-0 through buff-20 via repeated doubling: buff-20 = 1MB
contract.push_str("(define-constant buff-0 0x00)\n");
for i in 0..20 {
contract.push_str(&format!(
"(define-constant buff-{} (concat buff-{i} buff-{i}))\n",
i + 1
));
}

// Create a public function that makes many references to buff-20
contract.push_str("\n(define-public (create-many-references)\n");
contract.push_str(" (ok (is-eq ");

// Create 100 references to buff-20 (1MB each = ~100MB total)
for _ in 0..100 {
contract.push_str("buff-20 ");
}

contract.push_str(")))\n");
contract
},
function_name: "create-many-references",
function_args: &[],
);

/// Tests that when `MemoryBalanceExceeded` error occurs in a block, the transactions fail, but are
/// still committed to the block, and the "valid" transactions are still executed without errors.
///
/// 1. Deploy the memory-test-contract once in epoch 3.2
/// 2. Create a second block with 21 transactions:
/// - 20 transactions that call the contract (should fail with MemoryBalanceExceeded)
/// - 1 transaction that is expected to succeed
#[test]
fn test_memory_balance_exceeded_multiple_calls() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qq: shouldn't we rely on insta for all the consensus test?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the idea is that this in particular is not meant to be a consensus test but a more classic "integration test", I just used the new framework for it. But good thing you brought it up because this doesn't belong in here.
When we are going to merge your PR that makes the symbols public and ready to be used in other files, I'll move it somewhere else.

// Generate the same contract code as chainstate_error_memory_balance_exceeded_during_contract_call
let contract_code = {
// Procedurally generate a contract with large buffer constants and a function
// that creates many references to them, similar to argument_memory_test
let mut contract = String::new();

// Create buff-0 through buff-20 via repeated doubling: buff-20 = 1MB
contract.push_str("(define-constant buff-0 0x00)\n");
for i in 0..20 {
contract.push_str(&format!(
"(define-constant buff-{} (concat buff-{i} buff-{i}))\n",
i + 1
));
}

// Create a public function that makes many references to buff-20
contract.push_str("\n(define-public (create-many-references)\n");
contract.push_str(" (ok (is-eq ");

// Create 100 references to buff-20 (1MB each = ~100MB total)
for _ in 0..100 {
contract.push_str("buff-20 ");
}

contract.push_str(")))\n");
contract.push_str("(define-public (foo) (ok 1))");
contract
};

let mut nonce = 0;

// Add balance for the contract deployer (using FAUCET)
let deploy_fee = (contract_code.len() * 100) as u64;

// Block 1: Deploy the contract in epoch 3.2
let deploy_tx = make_contract_publish_versioned(
&FAUCET_PRIV_KEY,
nonce,
deploy_fee,
CHAIN_ID_TESTNET,
"memory-test-contract",
&contract_code,
Some(ClarityVersion::Clarity4),
);

let block1 = TestBlock {
transactions: vec![
StacksTransaction::consensus_deserialize(&mut deploy_tx.as_slice()).unwrap(),
],
};

// Block 2: 50 transactions calling the contract
let contract_addr =
StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&FAUCET_PRIV_KEY));
let mut call_transactions = Vec::new();

for _ in 0..20 {
nonce += 1;
let call_tx = make_contract_call(
&FAUCET_PRIV_KEY,
nonce,
200,
CHAIN_ID_TESTNET,
&contract_addr,
"memory-test-contract",
"create-many-references",
&[],
);
call_transactions
.push(StacksTransaction::consensus_deserialize(&mut call_tx.as_slice()).unwrap());
}
nonce += 1;
let foo_tx = make_contract_call(
&FAUCET_PRIV_KEY,
nonce,
200,
CHAIN_ID_TESTNET,
&contract_addr,
"memory-test-contract",
"foo",
&[],
);
call_transactions
.push(StacksTransaction::consensus_deserialize(&mut foo_tx.as_slice()).unwrap());

let block2 = TestBlock {
transactions: call_transactions,
};

// Create epoch blocks map - both blocks in Epoch 3.3
let mut epoch_blocks = HashMap::new();
epoch_blocks.insert(StacksEpochId::Epoch33, vec![block1, block2]);

// Run the test
let result = ConsensusTest::new(function_name!(), vec![]).run(epoch_blocks);
if let ExpectedResult::Success(expected_block_output) = &result[0] {
assert_eq!(expected_block_output.transactions.len(), 1);
assert_eq!(
expected_block_output.transactions[0].return_type,
ClarityValue::Response(ResponseData {
committed: true,
data: Box::new(ClarityValue::Bool(true)),
})
);
} else {
panic!("First block should be successful");
}
if let ExpectedResult::Success(expected_block_output) = &result[1] {
assert_eq!(expected_block_output.transactions.len(), 21);
let expected_vm_error = Some("MemoryBalanceExceeded(100665664, 100000000)".to_string());
let expected_failure_return_type = ClarityValue::Response(ResponseData {
committed: false,
data: Box::new(ClarityValue::Optional(OptionalData { data: None })),
});

for transaction in &expected_block_output.transactions[..20] {
assert_eq!(transaction.return_type, expected_failure_return_type);
assert_eq!(transaction.vm_error, expected_vm_error);
}
assert_eq!(
expected_block_output.transactions[20].return_type,
ClarityValue::Response(ResponseData {
committed: true,
data: Box::new(ClarityValue::Int(1)),
})
);
} else {
panic!("Second block should be successful");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: stackslib/src/chainstate/tests/consensus.rs
expression: result
---
[
Failure("Invalid Stacks block 5df30d42bad6b4b7388b82139b99ed39b3cd8418b51682195eb3a525df421f79: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
Failure("Invalid Stacks block be8cfcb7d8ccdfd04f092556c8fb4542b49e1a68c4a0ff7d495b5e7208a2be78: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
Failure("Invalid Stacks block 4b97bc26a6681b251bb2caecf2c89a0e90f87c589875c87046b395230621353f: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
Failure("Invalid Stacks block be2ff63675753dd6db20528d54de997b7e47842b3614965b2d44607df130b96f: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
Failure("Invalid Stacks block 368cee17382fa25fcdd4bab5c969e7568b60c8959da4648860121fc73a2ab450: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
Failure("Invalid Stacks block fadfe1c5a66fcf65b08201fa6673869b5590dba7d53ff854ae25e13f11f36315: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
Failure("Invalid Stacks block a20452a05233fb819119128353e32cd87255ce15b507166aa1645fea9c87e77c: CostOverflowError(ExecutionCost { write_length: 0, write_count: 0, read_length: 0, read_count: 0, runtime: 0 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 }, ExecutionCost { write_length: 18446744073709551615, write_count: 18446744073709551615, read_length: 18446744073709551615, read_count: 18446744073709551615, runtime: 18446744073709551615 })"),
]
Loading