From 8ff1c568b3613df067b8c2e3e4897fd4e2bd34fa Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Tue, 30 Sep 2025 17:45:58 +0100 Subject: [PATCH 1/7] refactor ConsensusTest for insta --- .github/workflows/stacks-core-tests.yml | 1 + Cargo.lock | 50 +++ Cargo.toml | 5 + stackslib/Cargo.toml | 1 + stackslib/src/chainstate/tests/consensus.rs | 345 +++++------------- ...pend_block_with_contract_upload_insta.snap | 28 ++ ...nd_block_with_contract_upload_success.snap | 28 ++ ...error_expression_stack_depth_too_deep.snap | 5 + ...sus__append_state_index_root_mismatch.snap | 5 + ...ests__consensus__append_stx_transfers.snap | 54 +++ 10 files changed, 271 insertions(+), 251 deletions(-) create mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_insta.snap create mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap create mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap create mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatch.snap create mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers.snap diff --git a/.github/workflows/stacks-core-tests.yml b/.github/workflows/stacks-core-tests.yml index 32b2e0ed61b..56adefa11c4 100644 --- a/.github/workflows/stacks-core-tests.yml +++ b/.github/workflows/stacks-core-tests.yml @@ -11,6 +11,7 @@ env: RUST_BACKTRACE: full SEGMENT_DOWNLOAD_TIMEOUT_MINS: 3 TEST_TIMEOUT: 30 + CI: true # Required by insta snapshot tests to run in CI concurrency: group: stacks-core-tests-${{ github.head_ref || github.ref || github.run_id }} diff --git a/Cargo.lock b/Cargo.lock index 1ef9cc74d59..0d33663687e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,6 +665,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -868,6 +880,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.33" @@ -1542,6 +1560,20 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "ron", + "serde", + "similar", +] + [[package]] name = "instant" version = "0.1.12" @@ -1797,6 +1829,12 @@ dependencies = [ "stacks-common 0.0.1 (git+https://github.com/stacks-network/stacks-core.git?rev=8a79aaa7df0f13dfc5ab0d0d0bcb8201c90bcba2)", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2580,6 +2618,17 @@ dependencies = [ "libc", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "serde", +] + [[package]] name = "rstest" version = "0.17.0" @@ -3311,6 +3360,7 @@ dependencies = [ "chrono", "clarity 0.0.1", "ed25519-dalek", + "insta", "lazy_static", "libstackerdb 0.0.1", "mio 0.6.23", diff --git a/Cargo.toml b/Cargo.toml index edbaa79db61..26b166c4834 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,3 +55,8 @@ lto = "fat" [profile.release-lite] inherits = "release" lto = "thin" + +# faster tests for `insta` https://docs.rs/insta/1.43.2/insta/#optional-faster-runs +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 diff --git a/stackslib/Cargo.toml b/stackslib/Cargo.toml index 7f8209b5824..6d2f85dfd04 100644 --- a/stackslib/Cargo.toml +++ b/stackslib/Cargo.toml @@ -79,6 +79,7 @@ rlimit = "0.10.2" chrono = "0.4.19" tempfile = "3.3" proptest = { version = "1.6.0", default-features = false, features = ["std"] } +insta = { version = "1.37.0", features = ["ron"] } [features] default = [] diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index f0828e903e0..971ef308f2e 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -12,6 +12,8 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::cell::LazyCell; + use clarity::boot_util::boot_code_addr; use clarity::codec::StacksMessageCodec; use clarity::consts::CHAIN_ID_TESTNET; @@ -22,25 +24,29 @@ use clarity::util::secp256k1::MessageSignature; use clarity::vm::ast::errors::{ParseError, ParseErrors}; use clarity::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER; use clarity::vm::costs::ExecutionCost; -use clarity::vm::events::StacksTransactionEvent; -use clarity::vm::types::{PrincipalData, ResponseData}; +use clarity::vm::types::PrincipalData; use clarity::vm::{Value as ClarityValue, MAX_CALL_STACK_DEPTH}; use serde::{Deserialize, Serialize}; use stacks_common::bitvec::BitVec; use crate::burnchains::PoxConstants; use crate::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader, NakamotoChainState}; -use crate::chainstate::stacks::boot::{RewardSet, RewardSetData}; +use crate::chainstate::stacks::boot::RewardSet; use crate::chainstate::stacks::db::StacksEpochReceipt; use crate::chainstate::stacks::{Error as ChainstateError, StacksTransaction, TenureChangeCause}; use crate::chainstate::tests::TestChainstate; -use crate::clarity_vm::clarity::{Error as ClarityError, PreCommitClarityBlock}; +use crate::clarity_vm::clarity::Error as ClarityError; use crate::core::test_util::{make_contract_publish, make_stacks_transfer_tx}; use crate::net::tests::NakamotoBootPlan; pub const SK_1: &str = "a1289f6438855da7decf9b61b852c882c398cff1446b2a0f823538aa2ebef92e01"; pub const SK_2: &str = "4ce9a8f7539ea93753a36405b16e8b57e15a552430410709c2b6d65dca5c02e201"; pub const SK_3: &str = "cb95ddd0fe18ec57f4f3533b95ae564b3f1ae063dbf75b46334bd86245aef78501"; +const FAUCET_PRIV_KEY: LazyCell = LazyCell::new(|| { + StacksPrivateKey::from_hex("510f96a8efd0b11e211733c1ac5e3fa6f3d3fcdd62869e376c47decb3e14fea101") + .expect("Failed to parse private key") +}); + /// Represents the expected output of a transaction in a test. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct ExpectedTransactionOutput { @@ -70,6 +76,29 @@ pub enum ExpectedResult { Failure(String), } +impl From> for ExpectedResult { + fn from(result: Result) -> Self { + match result { + Ok(epoch_receipt) => { + let transactions: Vec = epoch_receipt + .tx_receipts + .iter() + .map(|r| ExpectedTransactionOutput { + return_type: r.result.clone(), + cost: r.execution_cost.clone(), + }) + .collect(); + let total_block_cost = epoch_receipt.anchored_block_cost.clone(); + ExpectedResult::Success(ExpectedBlockOutput { + transactions, + total_block_cost, + }) + } + Err(e) => ExpectedResult::Failure(e.to_string()), + } + } +} + /// Defines a test vector for a consensus test, including chainstate setup and expected outcomes. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct ConsensusTestVector { @@ -81,171 +110,6 @@ pub struct ConsensusTestVector { pub epoch_id: u32, /// Transactions to include in the block pub transactions: Vec, - /// The expected result after appending the constructed block. - pub expected_result: ExpectedResult, -} - -/// Tracks mismatches between actual and expected transaction results. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct TransactionMismatch { - /// The index of the transaction with mismatches. - pub index: u32, - /// Mismatch between actual and expected return types, if any. - pub return_type: Option<(ClarityValue, ClarityValue)>, - /// Mismatch between actual and expected execution costs, if any. - pub cost: Option<(ExecutionCost, ExecutionCost)>, -} - -impl TransactionMismatch { - /// Creates a new `TransactionMismatch` for the given transaction index. - fn new(index: u32) -> Self { - Self { - index, - return_type: None, - cost: None, - } - } - - /// Adds a return type mismatch to the transaction. - fn with_return_type_mismatch(mut self, actual: ClarityValue, expected: ClarityValue) -> Self { - self.return_type = Some((actual, expected)); - self - } - - /// Adds an execution cost mismatch to the transaction. - fn with_cost_mismatch(mut self, actual: ExecutionCost, expected: ExecutionCost) -> Self { - self.cost = Some((actual, expected)); - self - } - - /// Returns true if no mismatches are recorded. - fn is_empty(&self) -> bool { - self.return_type.is_none() && self.cost.is_none() - } -} - -/// Aggregates all mismatches between actual and expected test results. -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] -pub struct ConsensusMismatch { - /// Mismatches for individual transactions. - pub transactions: Vec, - /// Mismatch between actual and expected total block costs, if any. - pub total_block_cost: Option<(ExecutionCost, ExecutionCost)>, - /// Mismatch between actual and expected error messages, if any. - pub error: Option<(String, String)>, -} - -impl ConsensusMismatch { - /// Creates a `ConsensusMismatch` from test results, if mismatches exist. - pub fn from_test_result<'a>( - append_result: Result< - ( - StacksEpochReceipt, - PreCommitClarityBlock<'a>, - Option, - Vec, - ), - ChainstateError, - >, - expected_result: ExpectedResult, - ) -> Option { - let mut mismatches = ConsensusMismatch::default(); - match (append_result, expected_result) { - (Ok((epoch_receipt, _, _, _)), ExpectedResult::Success(expected)) => { - // Convert transaction receipts to `ExpectedTransactionOutput` for comparison. - let actual_transactions: Vec<_> = epoch_receipt - .tx_receipts - .iter() - .map(|r| { - ( - r.tx_index, - ExpectedTransactionOutput { - return_type: r.result.clone(), - cost: r.execution_cost.clone(), - }, - ) - }) - .collect(); - - // Compare each transaction's actual vs expected outputs. - for ((tx_index, actual_tx), expected_tx) in - actual_transactions.iter().zip(expected.transactions.iter()) - { - let mut tx_mismatch = TransactionMismatch::new(*tx_index); - let mut has_mismatch = false; - - if actual_tx.return_type != expected_tx.return_type { - tx_mismatch = tx_mismatch.with_return_type_mismatch( - actual_tx.return_type.clone(), - expected_tx.return_type.clone(), - ); - has_mismatch = true; - } - - if actual_tx.cost != expected_tx.cost { - tx_mismatch = tx_mismatch - .with_cost_mismatch(actual_tx.cost.clone(), expected_tx.cost.clone()); - has_mismatch = true; - } - - if has_mismatch { - mismatches.add_transaction_mismatch(tx_mismatch); - } - } - - // Compare total block execution cost. - if epoch_receipt.anchored_block_cost != expected.total_block_cost { - mismatches.add_total_block_cost_mismatch( - &epoch_receipt.anchored_block_cost, - &expected.total_block_cost, - ); - } - // TODO: add any additional mismatches we might care about? - } - (Ok(_), ExpectedResult::Failure(expected_err)) => { - mismatches.error = Some(("Ok".to_string(), expected_err)); - } - (Err(actual_err), ExpectedResult::Failure(expected_err)) => { - let actual_err_str = actual_err.to_string(); - if actual_err_str != expected_err { - mismatches.error = Some((actual_err_str, expected_err)); - } - } - (Err(actual_err), ExpectedResult::Success(_)) => { - mismatches.error = Some((actual_err.to_string(), "Success".into())); - } - } - - if mismatches.is_empty() { - None - } else { - Some(mismatches) - } - } - - /// Adds a transaction mismatch to the collection. - fn add_transaction_mismatch(&mut self, mismatch: TransactionMismatch) { - self.transactions.push(mismatch); - } - - /// Records a total block cost mismatch. - fn add_total_block_cost_mismatch(&mut self, actual: &ExecutionCost, expected: &ExecutionCost) { - self.total_block_cost = Some((actual.clone(), expected.clone())); - } - - /// Returns true if no mismatches are recorded. - pub fn is_empty(&self) -> bool { - self.transactions.is_empty() && self.total_block_cost.is_none() && self.error.is_none() - } - - /// Serializes the given `ConsensusMismatch` as pretty-printed JSON, - /// or returns an empty string if `None`. - pub fn to_json_string_pretty(mismatch: &Option) -> String { - mismatch - .as_ref() - .map(|m| serde_json::to_string_pretty(m).unwrap()) - .unwrap_or("".into()) - } } /// Represents a consensus test with chainstate and test vector. @@ -257,18 +121,6 @@ pub struct ConsensusTest<'a> { impl ConsensusTest<'_> { /// Creates a new `ConsensusTest` with the given test name and vector. pub fn new(test_name: &str, test_vector: ConsensusTestVector) -> Self { - if let ExpectedResult::Success(output) = &test_vector.expected_result { - assert_eq!( - output.transactions.len(), - test_vector.transactions.len(), - "Test vector is invalid. Must specify an expected output per input transaction" - ); - } - let privk = StacksPrivateKey::from_hex( - "510f96a8efd0b11e211733c1ac5e3fa6f3d3fcdd62869e376c47decb3e14fea101", - ) - .unwrap(); - let epoch_id = StacksEpochId::try_from(test_vector.epoch_id).unwrap(); let chain = match epoch_id { StacksEpochId::Epoch30 @@ -278,7 +130,7 @@ impl ConsensusTest<'_> { let mut chain = NakamotoBootPlan::new(test_name) .with_pox_constants(10, 3) .with_initial_balances(test_vector.initial_balances.clone()) - .with_private_key(privk) + .with_private_key(FAUCET_PRIV_KEY.clone()) .boot_nakamoto_chainstate(None); let (burn_ops, mut tenure_change, miner_key) = chain.begin_nakamoto_tenure(TenureChangeCause::BlockFound); @@ -308,8 +160,11 @@ impl ConsensusTest<'_> { Self { chain, test_vector } } - /// Runs the consensus test, validating the results against the expected outcome. - pub fn run(mut self) { + /// Runs the consensus test. + /// + /// This method constructs a block from the test vector, appends it to the + /// chain, and returns the result of the block processing. + pub fn run(mut self) -> ExpectedResult { debug!("--------- Running test vector ---------"); let (block, block_size) = self.construct_nakamoto_block(); let mut stacks_node = self.chain.stacks_node.take().unwrap(); @@ -345,14 +200,8 @@ impl ConsensusTest<'_> { ); debug!("--------- Appended block: {} ---------", result.is_ok()); - // Compare actual vs expected results. - let mismatches = - ConsensusMismatch::from_test_result(result, self.test_vector.expected_result); - assert!( - mismatches.is_none(), - "Mismatches found: {}", - ConsensusMismatch::to_json_string_pretty(&mismatches) - ); + + result.map(|(receipt, _, _, _)| receipt).into() } /// Constructs a Nakamoto block with the given transactions and state index root. @@ -404,18 +253,26 @@ impl ConsensusTest<'_> { #[test] fn test_append_empty_block() { - let outputs = ExpectedBlockOutput { - transactions: vec![], - total_block_cost: ExecutionCost::ZERO, - }; let test_vector = ConsensusTestVector { initial_balances: Vec::new(), marf_hash: "6fe3e70b95f5f56c9c7c2c59ba8fc9c19cdfede25d2dcd4d120438bc27dfa88b".into(), epoch_id: StacksEpochId::Epoch30 as u32, transactions: vec![], - expected_result: ExpectedResult::Success(outputs), }; - ConsensusTest::new(function_name!(), test_vector).run() + let result = ConsensusTest::new(function_name!(), test_vector).run(); + // Example with inline expected result + insta::assert_ron_snapshot!(result, @r" + Success(ExpectedBlockOutput( + transactions: [], + total_block_cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, + runtime: 0, + ), + )) + "); } #[test] @@ -426,9 +283,10 @@ fn test_append_state_index_root_mismatch() { marf_hash: "0000000000000000000000000000000000000000000000000000000000000000".into(), epoch_id: StacksEpochId::Epoch30 as u32, transactions: vec![], - expected_result: ExpectedResult::Failure(ChainstateError::InvalidStacksBlock("Block c8eeff18a0b03dec385bfe8268bc87ccf93fc00ff73af600c4e1aaef6e0dfaf5 state root mismatch: expected 0000000000000000000000000000000000000000000000000000000000000000, got 6fe3e70b95f5f56c9c7c2c59ba8fc9c19cdfede25d2dcd4d120438bc27dfa88b".into()).to_string()), }; - ConsensusTest::new(function_name!(), test_vector).run() + + let result = ConsensusTest::new(function_name!(), test_vector).run(); + insta::assert_ron_snapshot!(result); } #[test] @@ -458,35 +316,15 @@ fn test_append_stx_transfers() { ) }) .collect(); - let transfer_result = ExpectedTransactionOutput { - return_type: ClarityValue::Response(ResponseData { - committed: true, - data: Box::new(ClarityValue::Bool(true)), - }), - cost: ExecutionCost { - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 0, - }, - }; - let outputs = ExpectedBlockOutput { - transactions: vec![ - transfer_result.clone(), - transfer_result.clone(), - transfer_result, - ], - total_block_cost: ExecutionCost::ZERO, - }; + let test_vector = ConsensusTestVector { initial_balances, marf_hash: "3838b1ae67f108b10ec7a7afb6c2b18e6468be2423d7183ffa2f7824b619b8be".into(), epoch_id: StacksEpochId::Epoch30 as u32, transactions, - expected_result: ExpectedResult::Success(outputs), }; - ConsensusTest::new(function_name!(), test_vector).run() + let result = ConsensusTest::new(function_name!(), test_vector).run(); + insta::assert_ron_snapshot!(result); } #[test] @@ -497,14 +335,9 @@ fn test_append_chainstate_error_expression_stack_depth_too_deep() { let tx_exceeds_body_end = "} ".repeat(exceeds_repeat_factor as usize); let tx_exceeds_body = format!("{tx_exceeds_body_start}u1 {tx_exceeds_body_end}"); - let sender_privk = StacksPrivateKey::from_hex(SK_1).unwrap(); let tx_fee = (tx_exceeds_body.len() * 100) as u64; - let initial_balances = vec![( - StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&sender_privk)).into(), - tx_fee, - )]; let tx_bytes = make_contract_publish( - &sender_privk, + &FAUCET_PRIV_KEY, 0, tx_fee, CHAIN_ID_TESTNET, @@ -512,37 +345,47 @@ fn test_append_chainstate_error_expression_stack_depth_too_deep() { &tx_exceeds_body, ); let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - let transfer_result = ExpectedTransactionOutput { - return_type: ClarityValue::Response(ResponseData { - committed: true, - data: Box::new(ClarityValue::Bool(true)), - }), - cost: ExecutionCost { - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 0, - }, - }; - let outputs = ExpectedBlockOutput { - transactions: vec![transfer_result], - total_block_cost: ExecutionCost::ZERO, - }; + // TODO: should look into append_block. It does weird wrapping of ChainstateError variants inside ChainstateError::StacksInvalidBlock. let e = ChainstateError::ClarityError(ClarityError::Parse(ParseError::new( ParseErrors::ExpressionStackDepthTooDeep, ))); let msg = format!("Invalid Stacks block 518dfea674b5c4874e025a31e01a522c8269005c0685d12658f0359757de6692: {e:?}"); let test_vector = ConsensusTestVector { - initial_balances, + initial_balances: vec![], // Marf hash doesn't matter. It will fail with ExpressionStackDepthTooDeep marf_hash: "0000000000000000000000000000000000000000000000000000000000000000".into(), epoch_id: StacksEpochId::Epoch30 as u32, transactions: vec![tx], - expected_result: ExpectedResult::Failure( - ChainstateError::InvalidStacksBlock(msg).to_string(), - ), }; - ConsensusTest::new(function_name!(), test_vector).run() + let result = ConsensusTest::new(function_name!(), test_vector).run(); + insta::assert_ron_snapshot!(result); +} + +#[test] +fn test_append_block_with_contract_upload_success() { + let contract_name = "test-contract"; + let contract_content = "(/ 1 1)"; + let tx_fee = (contract_content.len() * 100) as u64; + + let tx_bytes = make_contract_publish( + &FAUCET_PRIV_KEY, + 0, + tx_fee, + CHAIN_ID_TESTNET, + contract_name, + &contract_content, + ); + let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); + + let test_vector = ConsensusTestVector { + initial_balances: vec![], + marf_hash: "908f7e3a8c905d5ceabd3bcaced378038aec57e137034e35e29ddaaf738045b5".into(), + epoch_id: StacksEpochId::Epoch32 as u32, + transactions: vec![tx], + }; + + let result = ConsensusTest::new(function_name!(), test_vector).run(); + + insta::assert_ron_snapshot!(result); } diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_insta.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_insta.snap new file mode 100644 index 00000000000..cb0cd4b1655 --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_insta.snap @@ -0,0 +1,28 @@ +--- +source: stackslib/src/chainstate/tests/consensus.rs +expression: result +--- +Success(ExpectedBlockOutput( + transactions: [ + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 13, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 8114, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 13, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 8114, + ), +)) diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap new file mode 100644 index 00000000000..cb0cd4b1655 --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap @@ -0,0 +1,28 @@ +--- +source: stackslib/src/chainstate/tests/consensus.rs +expression: result +--- +Success(ExpectedBlockOutput( + transactions: [ + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 13, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 8114, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 13, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 8114, + ), +)) diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap new file mode 100644 index 00000000000..e48636252e0 --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap @@ -0,0 +1,5 @@ +--- +source: stackslib/src/chainstate/tests/consensus.rs +expression: result +--- +Failure("Invalid Stacks block 5822414b927f7f2da902f1009a894706bf0a51a56d239b3da3a501e8978ab6fb: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))") diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatch.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatch.snap new file mode 100644 index 00000000000..c478fb0387e --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatch.snap @@ -0,0 +1,5 @@ +--- +source: stackslib/src/chainstate/tests/consensus.rs +expression: result +--- +Failure("Block c8eeff18a0b03dec385bfe8268bc87ccf93fc00ff73af600c4e1aaef6e0dfaf5 state root mismatch: expected 0000000000000000000000000000000000000000000000000000000000000000, got 6fe3e70b95f5f56c9c7c2c59ba8fc9c19cdfede25d2dcd4d120438bc27dfa88b") diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers.snap new file mode 100644 index 00000000000..01c6d8fff8a --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers.snap @@ -0,0 +1,54 @@ +--- +source: stackslib/src/chainstate/tests/consensus.rs +expression: result +--- +Success(ExpectedBlockOutput( + transactions: [ + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, + runtime: 0, + ), + ), + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, + runtime: 0, + ), + ), + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, + runtime: 0, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, + runtime: 0, + ), +)) From f396663614f84146b12322f4a6d82e1308a4f6ef Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Wed, 1 Oct 2025 12:13:07 +0100 Subject: [PATCH 2/7] make FAUCET_PRIV_KEY pub --- stackslib/src/chainstate/tests/consensus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index 971ef308f2e..eb05ac4dbe9 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -42,7 +42,7 @@ pub const SK_1: &str = "a1289f6438855da7decf9b61b852c882c398cff1446b2a0f823538aa pub const SK_2: &str = "4ce9a8f7539ea93753a36405b16e8b57e15a552430410709c2b6d65dca5c02e201"; pub const SK_3: &str = "cb95ddd0fe18ec57f4f3533b95ae564b3f1ae063dbf75b46334bd86245aef78501"; -const FAUCET_PRIV_KEY: LazyCell = LazyCell::new(|| { +pub const FAUCET_PRIV_KEY: LazyCell = LazyCell::new(|| { StacksPrivateKey::from_hex("510f96a8efd0b11e211733c1ac5e3fa6f3d3fcdd62869e376c47decb3e14fea101") .expect("Failed to parse private key") }); From 74e9576d2f9949a9a893be1b9fcedea72c2230c7 Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Thu, 2 Oct 2025 10:33:03 +0100 Subject: [PATCH 3/7] remove unused code --- stackslib/src/chainstate/tests/consensus.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index 8cb7c5e98f1..858eef27155 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -27,7 +27,6 @@ use clarity::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKe use clarity::types::{StacksEpoch, StacksEpochId}; use clarity::util::hash::{MerkleTree, Sha512Trunc256Sum}; use clarity::util::secp256k1::MessageSignature; -use clarity::vm::ast::errors::{ParseError, ParseErrors}; use clarity::vm::ast::stack_depth_checker::AST_CALL_STACK_DEPTH_BUFFER; use clarity::vm::costs::ExecutionCost; use clarity::vm::types::PrincipalData; @@ -42,7 +41,6 @@ use crate::chainstate::stacks::boot::RewardSet; use crate::chainstate::stacks::db::StacksEpochReceipt; use crate::chainstate::stacks::{Error as ChainstateError, StacksTransaction, TenureChangeCause}; use crate::chainstate::tests::TestChainstate; -use crate::clarity_vm::clarity::Error as ClarityError; use crate::core::test_util::{make_contract_publish, make_stacks_transfer_tx}; use crate::core::{EpochList, BLOCK_LIMIT_MAINNET_21}; use crate::net::tests::NakamotoBootPlan; @@ -574,7 +572,6 @@ fn test_append_stx_transfers_success() { #[test] fn test_append_chainstate_error_expression_stack_depth_too_deep() { - let sender_privk = StacksPrivateKey::from_hex(SK_1).unwrap(); 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); @@ -591,13 +588,7 @@ fn test_append_chainstate_error_expression_stack_depth_too_deep() { ); let tx = StacksTransaction::consensus_deserialize(&mut &tx_bytes[..]).unwrap(); - let initial_balances = vec![( - StacksAddress::p2pkh(false, &StacksPublicKey::from_private(&sender_privk)).into(), - tx_fee, - )]; - let e = ChainstateError::ClarityError(ClarityError::Parse(ParseError::new( - ParseErrors::ExpressionStackDepthTooDeep, - ))); + let mut epoch_blocks = HashMap::new(); epoch_blocks.insert( StacksEpochId::Epoch30, @@ -629,7 +620,7 @@ fn test_append_chainstate_error_expression_stack_depth_too_deep() { ); let test_vector = ConsensusTestVector { epoch_blocks }; - let result = ConsensusTest::new(function_name!(), initial_balances).run(test_vector); + let result = ConsensusTest::new(function_name!(), vec![]).run(test_vector); insta::assert_ron_snapshot!(result); } From 7365051f405db39f0b25d37d6338243507b22ec5 Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Thu, 2 Oct 2025 10:33:40 +0100 Subject: [PATCH 4/7] fix test_append_state_index_root_mismatches --- stackslib/src/chainstate/tests/consensus.rs | 8 ++-- ...s__append_state_index_root_mismatches.snap | 44 ++----------------- 2 files changed, 8 insertions(+), 44 deletions(-) diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index 858eef27155..9375d426f8b 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -474,28 +474,28 @@ fn test_append_state_index_root_mismatches() { epoch_blocks.insert( StacksEpochId::Epoch30, vec![TestBlock { - marf_hash: "f1934080b22ef0192cfb39710690e7cb0efa9cff950832b33544bde3aa1484a5".into(), + marf_hash: "0000000000000000000000000000000000000000000000000000000000000000".into(), transactions: vec![], }], ); epoch_blocks.insert( StacksEpochId::Epoch31, vec![TestBlock { - marf_hash: "a05f1383613215f5789eb977e4c62dfbb789d90964e14865d109375f7f6dc3cf".into(), + marf_hash: "0000000000000000000000000000000000000000000000000000000000000000".into(), transactions: vec![], }], ); epoch_blocks.insert( StacksEpochId::Epoch32, vec![TestBlock { - marf_hash: "c17829daff8746329c65ae658f4087519c6a8bd8c7f21e51644ddbc9c010390f".into(), + marf_hash: "0000000000000000000000000000000000000000000000000000000000000000".into(), transactions: vec![], }], ); epoch_blocks.insert( StacksEpochId::Epoch33, vec![TestBlock { - marf_hash: "23ecbcb91cac914ba3994a15f3ea7189bcab4e9762530cd0e6c7d237fcd6dc78".into(), + marf_hash: "0000000000000000000000000000000000000000000000000000000000000000".into(), transactions: vec![], }], ); diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatches.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatches.snap index 017c5a91da9..1d738395369 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatches.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_state_index_root_mismatches.snap @@ -3,44 +3,8 @@ source: stackslib/src/chainstate/tests/consensus.rs expression: result --- [ - Success(ExpectedBlockOutput( - transactions: [], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 0, - ), - )), - Success(ExpectedBlockOutput( - transactions: [], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 0, - ), - )), - Success(ExpectedBlockOutput( - transactions: [], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 0, - ), - )), - Success(ExpectedBlockOutput( - transactions: [], - total_block_cost: ExecutionCost( - write_length: 0, - write_count: 0, - read_length: 0, - read_count: 0, - runtime: 0, - ), - )), + Failure("Block ef45bfa44231d9e7aff094b53cfd48df0456067312f169a499354c4273a66fe3 state root mismatch: expected 0000000000000000000000000000000000000000000000000000000000000000, got f1934080b22ef0192cfb39710690e7cb0efa9cff950832b33544bde3aa1484a5"), + Failure("Block a14d0b5c8d3c49554aeb462a8fe019718195789fa1dcd642059b75e41f0ce9cc state root mismatch: expected 0000000000000000000000000000000000000000000000000000000000000000, got a05f1383613215f5789eb977e4c62dfbb789d90964e14865d109375f7f6dc3cf"), + Failure("Block f8120b4a632ee1d49fbbde3e01289588389cd205cab459a4493a7d58d2dc18ed state root mismatch: expected 0000000000000000000000000000000000000000000000000000000000000000, got c17829daff8746329c65ae658f4087519c6a8bd8c7f21e51644ddbc9c010390f"), + Failure("Block 4dcb48b684d105ff0e0ab8becddd4a2d5623cc8b168aacf9c455e20b3e610e63 state root mismatch: expected 0000000000000000000000000000000000000000000000000000000000000000, got 23ecbcb91cac914ba3994a15f3ea7189bcab4e9762530cd0e6c7d237fcd6dc78"), ] From ce05f1d3a096a0524017a05cf6a25bd2683ae06e Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Thu, 2 Oct 2025 12:27:50 +0100 Subject: [PATCH 5/7] revert to previous ConsensusTest signature. update tests --- stackslib/src/chainstate/tests/consensus.rs | 133 ++++++++++++------ ...nd_block_with_contract_upload_success.snap | 102 -------------- ...error_expression_stack_depth_too_deep.snap | 8 +- 3 files changed, 95 insertions(+), 148 deletions(-) delete mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index 9375d426f8b..9412dd76dda 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -210,6 +210,8 @@ pub struct TestBlock { /// Defines a test vector for a consensus test, including chainstate setup and expected outcomes. #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] pub struct ConsensusTestVector { + /// Initial balances for the provided PrincipalData during chainstate instantiation. + pub initial_balances: Vec<(PrincipalData, u64)>, /// A mapping of epoch to Blocks that should be applied in that epoch pub epoch_blocks: HashMap>, } @@ -217,17 +219,40 @@ pub struct ConsensusTestVector { /// Represents a consensus test with chainstate and test vector. pub struct ConsensusTest<'a> { pub chain: TestChainstate<'a>, + pub test_vector: ConsensusTestVector, } impl ConsensusTest<'_> { /// Creates a new `ConsensusTest` with the given test name and vector. - pub fn new(test_name: &str, initial_balances: Vec<(PrincipalData, u64)>) -> Self { + pub fn new(test_name: &str, test_vector: ConsensusTestVector) -> Self { + // Validate blocks + for (epoch_id, blocks) in &test_vector.epoch_blocks { + assert!( + !matches!( + *epoch_id, + StacksEpochId::Epoch10 + | StacksEpochId::Epoch20 + | StacksEpochId::Epoch2_05 + | StacksEpochId::Epoch21 + | StacksEpochId::Epoch22 + | StacksEpochId::Epoch23 + | StacksEpochId::Epoch24 + | StacksEpochId::Epoch25 + ), + "Pre-Nakamoto Tenures are not Supported" + ); + assert!( + !blocks.is_empty(), + "Each epoch must have at least one block" + ); + } + // Set up chainstate to start at Epoch 3.0 // We don't really ever want the reward cycle to force a new signer set... // so for now just set the cycle length to a high value (100) let mut boot_plan = NakamotoBootPlan::new(test_name) .with_pox_constants(100, 3) - .with_initial_balances(initial_balances.clone()) + .with_initial_balances(test_vector.initial_balances.clone()) .with_private_key(FAUCET_PRIV_KEY.clone()); let epochs = epoch_3_0_onwards( (boot_plan.pox_constants.pox_4_activation_height @@ -237,7 +262,7 @@ impl ConsensusTest<'_> { boot_plan = boot_plan.with_epochs(epochs); let chain = boot_plan.boot_nakamoto_chainstate(None); - Self { chain } + Self { chain, test_vector } } /// Advances the chainstate to the specified epoch. Creating a tenure change block per burn block height @@ -277,41 +302,20 @@ impl ConsensusTest<'_> { /// /// This method constructs a block from the test vector, appends it to the /// chain, and returns the result of the block processing. - pub fn run(mut self, test_vector: ConsensusTestVector) -> Vec { - // Validate blocks - for (epoch_id, blocks) in &test_vector.epoch_blocks { - assert!( - !matches!( - *epoch_id, - StacksEpochId::Epoch10 - | StacksEpochId::Epoch20 - | StacksEpochId::Epoch2_05 - | StacksEpochId::Epoch21 - | StacksEpochId::Epoch22 - | StacksEpochId::Epoch23 - | StacksEpochId::Epoch24 - | StacksEpochId::Epoch25 - ), - "Pre-Nakamoto Tenures are not Supported" - ); - assert!( - !blocks.is_empty(), - "Each epoch must have at least one block" - ); - } - + pub fn run(mut self) -> Vec { // Get sorted epochs - let mut epochs: Vec = test_vector.epoch_blocks.keys().cloned().collect(); + let mut epochs: Vec = + self.test_vector.epoch_blocks.keys().cloned().collect(); epochs.sort(); let mut results = vec![]; for epoch in epochs { debug!( "--------- Processing epoch {epoch:?} with {} blocks ---------", - test_vector.epoch_blocks[&epoch].len() + self.test_vector.epoch_blocks[&epoch].len() ); self.advance_to_epoch(epoch); - for (i, block) in test_vector.epoch_blocks[&epoch].iter().enumerate() { + for (i, block) in self.test_vector.epoch_blocks[&epoch].iter().enumerate() { debug!("--------- Running block {i} for epoch {epoch:?} ---------"); let (nakamoto_block, block_size) = self.construct_nakamoto_block(&block.marf_hash, &block.transactions); @@ -463,8 +467,11 @@ fn test_append_empty_blocks() { }], ); - let test_vector = ConsensusTestVector { epoch_blocks }; - let result = ConsensusTest::new(function_name!(), vec![]).run(test_vector); + let test_vector = ConsensusTestVector { + initial_balances: vec![], + epoch_blocks, + }; + let result = ConsensusTest::new(function_name!(), test_vector).run(); insta::assert_ron_snapshot!(result); } @@ -500,8 +507,11 @@ fn test_append_state_index_root_mismatches() { }], ); - let test_vector = ConsensusTestVector { epoch_blocks }; - let result = ConsensusTest::new(function_name!(), vec![]).run(test_vector); + let test_vector = ConsensusTestVector { + initial_balances: vec![], + epoch_blocks, + }; + let result = ConsensusTest::new(function_name!(), test_vector).run(); insta::assert_ron_snapshot!(result); } @@ -564,9 +574,12 @@ fn test_append_stx_transfers_success() { }], ); - let test_vector = ConsensusTestVector { epoch_blocks }; + let test_vector = ConsensusTestVector { + initial_balances, + epoch_blocks, + }; - let result = ConsensusTest::new(function_name!(), initial_balances).run(test_vector); + let result = ConsensusTest::new(function_name!(), test_vector).run(); insta::assert_ron_snapshot!(result); } @@ -619,8 +632,11 @@ fn test_append_chainstate_error_expression_stack_depth_too_deep() { }], ); - let test_vector = ConsensusTestVector { epoch_blocks }; - let result = ConsensusTest::new(function_name!(), vec![]).run(test_vector); + let test_vector = ConsensusTestVector { + initial_balances: vec![], + epoch_blocks, + }; + let result = ConsensusTest::new(function_name!(), test_vector).run(); insta::assert_ron_snapshot!(result); } @@ -669,9 +685,42 @@ fn test_append_block_with_contract_upload_success() { transactions: vec![tx.clone()], }], ); - let test_vector = ConsensusTestVector { epoch_blocks }; - - let result = ConsensusTest::new(function_name!(), vec![]).run(test_vector); - - insta::assert_ron_snapshot!(result); + let test_vector = ConsensusTestVector { + initial_balances: vec![], + epoch_blocks, + }; + + let result = ConsensusTest::new(function_name!(), test_vector).run(); + // Example of expecting the same result across all blocks + insta::allow_duplicates! { + for res in result { + // Example of inline snapshot + insta::assert_ron_snapshot!(res, @r" + Success(ExpectedBlockOutput( + transactions: [ + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 13, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 8114, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 13, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 8114, + ), + )) + "); + } + } } diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap deleted file mode 100644 index e3289157277..00000000000 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_upload_success.snap +++ /dev/null @@ -1,102 +0,0 @@ ---- -source: stackslib/src/chainstate/tests/consensus.rs -expression: result ---- -[ - Success(ExpectedBlockOutput( - transactions: [ - ExpectedTransactionOutput( - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - )), - Success(ExpectedBlockOutput( - transactions: [ - ExpectedTransactionOutput( - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - )), - Success(ExpectedBlockOutput( - transactions: [ - ExpectedTransactionOutput( - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - )), - Success(ExpectedBlockOutput( - transactions: [ - ExpectedTransactionOutput( - return_type: Response(ResponseData( - committed: true, - data: Bool(true), - )), - cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - ), - ], - total_block_cost: ExecutionCost( - write_length: 13, - write_count: 2, - read_length: 1, - read_count: 1, - runtime: 8114, - ), - )), -] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap index ebc055d698a..6089465bfa3 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap @@ -3,8 +3,8 @@ source: stackslib/src/chainstate/tests/consensus.rs expression: result --- [ - Failure("Invalid Stacks block a441b00c9a2d5093524dc87161b644c15cde93948c5b28e54425326e8d59961d: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block 62a2f769bc70d73f47bcbf8cb268fe03d9e361788fe77afa195768c90a91f488: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block a489609c924c0eaea59b91a0b34595d10f03ca5f29cbd3fcecfe7fc9a30e210f: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block 3b20a227a540976766393d40929a86f384601f27f652dc97328879d6708ee400: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block a60c62267d58f1ea29c64b2f86d62cf210ff5ab14796abfa947ca6d95007d440: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block 238f2ce280580228f19c8122a9bdd0c61299efabe59d8c22c315ee40a865cc7b: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block b5dd8cdc0f48b30d355a950077f7c9b20bf01062e9c96262c28f17fff55a2b0f: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block cfbddc874c465753158a065eff61340e933d33671633843dde0fbd2bfaaac7a4: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), ] From 5b2dc39ba226e3a51fe0b25dd171401c35ed32b6 Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Thu, 2 Oct 2025 15:19:52 +0100 Subject: [PATCH 6/7] add contract_call example --- stackslib/src/chainstate/tests/consensus.rs | 81 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index 9412dd76dda..585b3f6a073 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -41,7 +41,9 @@ use crate::chainstate::stacks::boot::RewardSet; use crate::chainstate::stacks::db::StacksEpochReceipt; use crate::chainstate::stacks::{Error as ChainstateError, StacksTransaction, TenureChangeCause}; use crate::chainstate::tests::TestChainstate; -use crate::core::test_util::{make_contract_publish, make_stacks_transfer_tx}; +use crate::core::test_util::{ + make_contract_call, make_contract_publish, make_stacks_transfer_tx, to_addr, +}; use crate::core::{EpochList, BLOCK_LIMIT_MAINNET_21}; use crate::net::tests::NakamotoBootPlan; @@ -55,6 +57,9 @@ pub const FAUCET_PRIV_KEY: LazyCell = LazyCell::new(|| { .expect("Failed to parse private key") }); +const FOO_CONTRACT: &str = "(define-public (foo) (ok 1)) + (define-public (bar (x uint)) (ok x))"; + fn epoch_3_0_onwards(first_burnchain_height: u64) -> EpochList { info!("StacksEpoch unit_test first_burn_height = {first_burnchain_height}"); @@ -724,3 +729,77 @@ fn test_append_block_with_contract_upload_success() { } } } + +#[test] +fn test_append_block_with_contract_call_success() { + let tx_fee = (FOO_CONTRACT.len() * 100) as u64; + + let tx_bytes = make_contract_publish( + &FAUCET_PRIV_KEY, + 0, + tx_fee, + CHAIN_ID_TESTNET, + "foo_contract", + FOO_CONTRACT, + ); + let tx_contract_deploy = + StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap(); + + let tx_bytes = make_contract_call( + &FAUCET_PRIV_KEY, + 1, + 200, + CHAIN_ID_TESTNET, + &to_addr(&FAUCET_PRIV_KEY), + "foo_contract", + "bar", + &[ClarityValue::UInt(1)], + ); + let tx_contract_call = + StacksTransaction::consensus_deserialize(&mut tx_bytes.as_slice()).unwrap(); + + let mut epoch_blocks = HashMap::new(); + epoch_blocks.insert( + StacksEpochId::Epoch30, + vec![TestBlock { + marf_hash: "186c8e49bcfc59bb67ed22f031f009a44681f296392e0f92bed520918ba463ae".into(), + transactions: vec![tx_contract_deploy.clone(), tx_contract_call.clone()], + }], + ); + + epoch_blocks.insert( + StacksEpochId::Epoch31, + vec![TestBlock { + marf_hash: "ad23713f072473cad6a32125ed5fa822bb62bbfae8ed2302209c12d2f1958128".into(), + transactions: vec![tx_contract_deploy.clone(), tx_contract_call.clone()], + }], + ); + + epoch_blocks.insert( + StacksEpochId::Epoch32, + vec![TestBlock { + marf_hash: "021bd30b09b5ac6ff34abd11f05244a966af937b584b1752f272cd717bb25f1d".into(), + transactions: vec![tx_contract_deploy.clone(), tx_contract_call.clone()], + }], + ); + + epoch_blocks.insert( + StacksEpochId::Epoch33, + vec![TestBlock { + marf_hash: "416e728daeec4de695c89d15eede8ddb7b85fb4af82daffb1e0d8166a3e93451".into(), + transactions: vec![tx_contract_deploy, tx_contract_call], + }], + ); + + let test_vector = ConsensusTestVector { + initial_balances: vec![], + epoch_blocks, + }; + + let result = ConsensusTest::new(function_name!(), test_vector).run(); + insta::allow_duplicates! { + for res in result { + insta::assert_ron_snapshot!(res); + } + } +} From f34fde55ea356faa5d81398d3056ff8e992ad421 Mon Sep 17 00:00:00 2001 From: Francesco Leacche Date: Fri, 3 Oct 2025 17:03:36 +0100 Subject: [PATCH 7/7] add missing snapshot --- ...pend_block_with_contract_call_success.snap | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap new file mode 100644 index 00000000000..f50380ee572 --- /dev/null +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap @@ -0,0 +1,41 @@ +--- +source: stackslib/src/chainstate/tests/consensus.rs +expression: res +--- +Success(ExpectedBlockOutput( + transactions: [ + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: Bool(true), + )), + cost: ExecutionCost( + write_length: 121, + write_count: 2, + read_length: 1, + read_count: 1, + runtime: 11968, + ), + ), + ExpectedTransactionOutput( + return_type: Response(ResponseData( + committed: true, + data: UInt(1), + )), + cost: ExecutionCost( + write_length: 0, + write_count: 0, + read_length: 103, + read_count: 3, + runtime: 499, + ), + ), + ], + total_block_cost: ExecutionCost( + write_length: 121, + write_count: 2, + read_length: 104, + read_count: 4, + runtime: 12467, + ), +))