From 1a6a75591ed7fb673b6eba55ec256d5d2a1dc2dd Mon Sep 17 00:00:00 2001 From: Roberto De Ioris Date: Wed, 8 Oct 2025 14:04:01 +0200 Subject: [PATCH] added vm_error field for transaction block replay --- .../schemas/block-replay.schema.yaml | 5 +- stackslib/src/net/api/blockreplay.rs | 3 + stackslib/src/net/api/tests/blockreplay.rs | 93 ++++++++++++++++++- stackslib/src/net/tests/inv/nakamoto.rs | 23 +++++ stackslib/src/net/tests/mod.rs | 27 +++++- 5 files changed, 144 insertions(+), 7 deletions(-) diff --git a/docs/rpc/components/schemas/block-replay.schema.yaml b/docs/rpc/components/schemas/block-replay.schema.yaml index c8e9802bb2..05fa71847e 100644 --- a/docs/rpc/components/schemas/block-replay.schema.yaml +++ b/docs/rpc/components/schemas/block-replay.schema.yaml @@ -75,4 +75,7 @@ properties: description: index of the transaction in the array of transactions txid: type: string - description: transaction id \ No newline at end of file + description: transaction id + vm_error: + type: string + description: optional vm error (for runtime failures) \ No newline at end of file diff --git a/stackslib/src/net/api/blockreplay.rs b/stackslib/src/net/api/blockreplay.rs index 2d3fe50e44..bbebba2f2b 100644 --- a/stackslib/src/net/api/blockreplay.rs +++ b/stackslib/src/net/api/blockreplay.rs @@ -218,6 +218,8 @@ pub struct RPCReplayedBlockTransaction { pub execution_cost: ExecutionCost, /// generated events pub events: Vec, + /// optional vm error + pub vm_error: Option, } impl RPCReplayedBlockTransaction { @@ -249,6 +251,7 @@ impl RPCReplayedBlockTransaction { stx_burned: receipt.stx_burned, execution_cost: receipt.execution_cost.clone(), events, + vm_error: receipt.vm_error.clone(), } } } diff --git a/stackslib/src/net/api/tests/blockreplay.rs b/stackslib/src/net/api/tests/blockreplay.rs index 31e727727b..1f243f138e 100644 --- a/stackslib/src/net/api/tests/blockreplay.rs +++ b/stackslib/src/net/api/tests/blockreplay.rs @@ -16,15 +16,18 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use stacks_common::consts::CHAIN_ID_TESTNET; use stacks_common::types::chainstate::StacksBlockId; -use crate::chainstate::stacks::Error as ChainError; +use crate::chainstate::stacks::{Error as ChainError, StacksTransaction}; +use crate::core::test_util::make_contract_publish; use crate::net::api::blockreplay; use crate::net::api::tests::TestRPC; use crate::net::connection::ConnectionOptions; use crate::net::httpcore::{StacksHttp, StacksHttpRequest}; use crate::net::test::TestEventObserver; use crate::net::ProtocolFamily; +use crate::stacks_common::codec::StacksMessageCodec; #[test] fn test_try_parse_request() { @@ -179,3 +182,91 @@ fn test_try_make_response() { let (preamble, body) = response.destruct(); assert_eq!(preamble.status_code, 401); } + +#[test] +fn test_try_make_response_with_unsuccessful_transaction() { + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 33333); + + let test_observer = TestEventObserver::new(); + let rpc_test = + TestRPC::setup_nakamoto_with_boot_plan(function_name!(), &test_observer, |boot_plan| { + let mut tip_transactions: Vec = vec![]; + + let miner_privk = boot_plan.private_key.clone(); + + let contract_code = "(broken)"; + + let deploy_tx_bytes = make_contract_publish( + &miner_privk, + 100, + 1000, + CHAIN_ID_TESTNET, + &"err-contract", + &contract_code, + ); + let deploy_tx = + StacksTransaction::consensus_deserialize(&mut deploy_tx_bytes.as_slice()).unwrap(); + + tip_transactions.push(deploy_tx); + boot_plan + .with_tip_transactions(tip_transactions) + .with_ignore_transaction_errors(true) + }); + + let tip_block = test_observer.get_blocks().last().unwrap().clone(); + + let nakamoto_consensus_hash = rpc_test.consensus_hash.clone(); + + let mut requests = vec![]; + + let mut request = + StacksHttpRequest::new_block_replay(addr.clone().into(), &rpc_test.canonical_tip); + // add the authorization header + request.add_header("authorization".into(), "password".into()); + requests.push(request); + + let mut responses = rpc_test.run(requests); + + // got the Nakamoto tip + let response = responses.remove(0); + + debug!( + "Response:\n{}\n", + std::str::from_utf8(&response.try_serialize().unwrap()).unwrap() + ); + + let resp = response.decode_replayed_block().unwrap(); + + assert_eq!(resp.consensus_hash, nakamoto_consensus_hash); + assert_eq!(resp.consensus_hash, tip_block.metadata.consensus_hash); + + assert_eq!(resp.block_hash, tip_block.block.block_hash); + assert_eq!(resp.block_id, tip_block.metadata.index_block_hash()); + assert_eq!(resp.parent_block_id, tip_block.parent); + + assert_eq!(resp.block_height, tip_block.metadata.stacks_block_height); + + assert!(resp.valid_merkle_root); + + assert_eq!(resp.transactions.len(), tip_block.receipts.len()); + + for tx_index in 0..resp.transactions.len() { + assert_eq!( + resp.transactions[tx_index].txid, + tip_block.receipts[tx_index].transaction.txid() + ); + assert_eq!( + resp.transactions[tx_index].events.len(), + tip_block.receipts[tx_index].events.len() + ); + assert_eq!( + resp.transactions[tx_index].result, + tip_block.receipts[tx_index].result + ); + } + + assert_eq!( + resp.transactions.last().unwrap().vm_error.clone().unwrap(), + ":0:0: use of unresolved function 'broken'" + ); +} diff --git a/stackslib/src/net/tests/inv/nakamoto.rs b/stackslib/src/net/tests/inv/nakamoto.rs index a510c992c8..9242c4c927 100644 --- a/stackslib/src/net/tests/inv/nakamoto.rs +++ b/stackslib/src/net/tests/inv/nakamoto.rs @@ -515,6 +515,29 @@ where plan.initial_balances.append(&mut initial_balances); + if !plan.tip_transactions.is_empty() { + let mut tip_transactions = plan.tip_transactions.clone(); + if let Some(tip_tenure) = boot_tenures.last_mut() { + match tip_tenure { + NakamotoBootTenure::Sortition(boot_steps) => match boot_steps.last_mut().unwrap() { + NakamotoBootStep::Block(transactions) => { + transactions.append(&mut tip_transactions) + } + _ => (), + }, + NakamotoBootTenure::NoSortition(boot_steps) => { + let boot_steps_len = boot_steps.len(); + match boot_steps.get_mut(boot_steps_len - 2).unwrap() { + NakamotoBootStep::Block(transactions) => { + transactions.append(&mut tip_transactions) + } + _ => (), + } + } + } + } + } + let (peer, other_peers) = plan.boot_into_nakamoto_peers(boot_tenures, Some(observer)); (peer, other_peers) } diff --git a/stackslib/src/net/tests/mod.rs b/stackslib/src/net/tests/mod.rs index 57859fed31..6fb0cc1071 100644 --- a/stackslib/src/net/tests/mod.rs +++ b/stackslib/src/net/tests/mod.rs @@ -104,6 +104,8 @@ pub struct NakamotoBootPlan { pub network_id: u32, pub txindex: bool, pub epochs: Option>, + pub tip_transactions: Vec, + pub ignore_transaction_errors: bool, } impl NakamotoBootPlan { @@ -124,6 +126,8 @@ impl NakamotoBootPlan { network_id: TestPeerConfig::default().network_id, txindex: false, epochs: None, + tip_transactions: vec![], + ignore_transaction_errors: false, } } @@ -169,6 +173,16 @@ impl NakamotoBootPlan { self } + pub fn with_tip_transactions(mut self, tip_transactions: Vec) -> Self { + self.tip_transactions = tip_transactions; + self + } + + pub fn with_ignore_transaction_errors(mut self, ignore_transaction_errors: bool) -> Self { + self.ignore_transaction_errors = ignore_transaction_errors; + self + } + pub fn with_test_stackers(mut self, test_stackers: Vec) -> Self { self.test_stackers = test_stackers; self @@ -891,6 +905,7 @@ impl NakamotoBootPlan { let test_signers = self.test_signers.clone(); let pox_constants = self.pox_constants.clone(); let test_stackers = self.test_stackers.clone(); + let ignore_transaction_errors = self.ignore_transaction_errors; let (mut peer, mut other_peers) = self.boot_nakamoto_peers(observer); if boot_plan.is_empty() { @@ -1191,11 +1206,13 @@ impl NakamotoBootPlan { // transactions processed in the same order assert_eq!(receipt.transaction.txid(), tx.txid()); // no CheckErrors - assert!( - receipt.vm_error.is_none(), - "Receipt had a CheckErrors: {:?}", - &receipt - ); + if !ignore_transaction_errors { + assert!( + receipt.vm_error.is_none(), + "Receipt had a CheckErrors: {:?}", + &receipt + ); + } // transaction was not aborted post-hoc assert!(!receipt.post_condition_aborted); }