Skip to content
5 changes: 4 additions & 1 deletion docs/rpc/components/schemas/block-replay.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,7 @@ properties:
description: index of the transaction in the array of transactions
txid:
type: string
description: transaction id
description: transaction id
vm_error:
type: [string, "null"]
description: optional vm error (for runtime failures)
3 changes: 3 additions & 0 deletions stackslib/src/net/api/blockreplay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ pub struct RPCReplayedBlockTransaction {
pub execution_cost: ExecutionCost,
/// generated events
pub events: Vec<serde_json::Value>,
/// optional vm error
pub vm_error: Option<String>,
}

impl RPCReplayedBlockTransaction {
Expand Down Expand Up @@ -252,6 +254,7 @@ impl RPCReplayedBlockTransaction {
stx_burned: receipt.stx_burned,
execution_cost: receipt.execution_cost.clone(),
events,
vm_error: receipt.vm_error.clone(),
}
}
}
Expand Down
93 changes: 92 additions & 1 deletion stackslib/src/net/api/tests/blockreplay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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<StacksTransaction> = 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'"
);
}
25 changes: 25 additions & 0 deletions stackslib/src/net/tests/inv/nakamoto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,31 @@ 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();
// when NakamotoBootTenure::NoSortition is in place we have every NakamotoBootStep::Block
// followed by NakamotoBootStep::TenureExtend (this is why we index by boot_steps_len - 2)
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)
}
Expand Down
29 changes: 24 additions & 5 deletions stackslib/src/net/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ pub struct NakamotoBootPlan {
pub network_id: u32,
pub txindex: bool,
pub epochs: Option<EpochList<ExecutionCost>>,
/// Additional transactions to include in the tip block
pub tip_transactions: Vec<StacksTransaction>,
/// Do not fail if a transaction returns error (by default the BootPlan will stop on tx failure)
pub ignore_transaction_errors: bool,
}

impl NakamotoBootPlan {
Expand All @@ -123,6 +127,8 @@ impl NakamotoBootPlan {
network_id: default_config.network_id,
txindex: false,
epochs: None,
tip_transactions: vec![],
ignore_transaction_errors: false,
}
}

Expand Down Expand Up @@ -222,6 +228,16 @@ impl NakamotoBootPlan {
self
}

pub fn with_tip_transactions(mut self, tip_transactions: Vec<StacksTransaction>) -> 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<TestStacker>) -> Self {
self.test_stackers = test_stackers;
self
Expand Down Expand Up @@ -476,6 +492,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() {
Expand Down Expand Up @@ -824,11 +841,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);
}
Expand Down
Loading