diff --git a/CHANGELOG.md b/CHANGELOG.md index 2088a70ca1..50278d8e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE ### Added +- Added field `vm_error` to EventObserver transaction outputs - Added new `ValidateRejectCode` values to the `/v3/block_proposal` endpoint ### Changed diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index 3d032d4c8a..0eb0ab62b8 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -17,7 +17,8 @@ use std::{error, fmt, fs, io}; use clarity::vm::types::PrincipalData; -use serde::Deserialize; +use serde::de::Error as DeError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde_json::json; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockId, TrieHash, VRFSeed, @@ -374,6 +375,132 @@ pub fn stacks_addr_serialize(addr: &StacksAddress) -> serde_json::Value { }) } +fn normalize_stacks_addr_fields<'de, D>( + inner: &mut serde_json::Map, +) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + // Rename `address_version` to `version` + if let Some(address_version) = inner.remove("address_version") { + inner.insert("version".to_string(), address_version); + } + + // Rename `address_hash_bytes` to `bytes` and convert to bytes + if let Some(address_bytes) = inner + .remove("address_hash_bytes") + .and_then(|addr| serde_json::Value::as_str(&addr).map(|x| x.to_string())) + { + let address_hex: String = address_bytes.chars().skip(2).collect(); // Remove "0x" prefix + inner.insert( + "bytes".to_string(), + serde_json::to_value(&address_hex).map_err(DeError::custom)?, + ); + } + + Ok(()) +} + +/// Serialization function for serializing extended information within the BlockstackOperationType +/// that is not printed via the standard serde implementation. Specifically, serializes additional +/// StacksAddress information. +pub fn blockstack_op_extended_serialize_opt( + op: &Option, + s: S, +) -> Result { + match op { + Some(op) => { + let value = op.blockstack_op_to_json(); + value.serialize(s) + } + None => s.serialize_none(), + } +} + +/// Deserialize the burnchain op that was serialized with blockstack_op_to_json +pub fn blockstack_op_extended_deserialize<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + use serde::de::Error as DeError; + use serde_json::{Map, Value}; + + let raw: Option = Option::deserialize(deserializer)?; + let Some(Value::Object(mut obj)) = raw else { + return Ok(None); + }; + + let Some((key, value)) = obj.iter_mut().next() else { + return Ok(None); + }; + + let inner = value + .as_object_mut() + .ok_or_else(|| DeError::custom("Expected blockstack op to be an object"))?; + + let normalized_key = match key.as_str() { + "pre_stx" => { + BlockstackOperationType::normalize_pre_stx_fields::(inner)?; + "PreStx" + } + "stack_stx" => { + BlockstackOperationType::normalize_stack_stx_fields::(inner)?; + "StackStx" + } + "transfer_stx" => { + BlockstackOperationType::normalize_transfer_stx_fields::(inner)?; + "TransferStx" + } + "delegate_stx" => { + BlockstackOperationType::normalize_delegate_stx_fields::(inner)?; + "DelegateStx" + } + "vote_for_aggregate_key" => { + BlockstackOperationType::normalize_vote_for_aggregate_key_fields::(inner)?; + "VoteForAggregateKey" + } + "leader_key_register" => "LeaderKeyRegister", + "leader_block_commit" => "LeaderBlockCommit", + other => other, + }; + + let mut map = Map::new(); + map.insert(normalized_key.to_string(), value.clone()); + + let normalized = Value::Object(map); + + serde_json::from_value(normalized) + .map(Some) + .map_err(serde::de::Error::custom) +} + +fn normalize_common_fields<'de, D>( + map: &mut serde_json::Map, +) -> Result<(), D::Error> +where + D: Deserializer<'de>, +{ + if let Some(hex_str) = map + .get("burn_header_hash") + .and_then(serde_json::Value::as_str) + { + let cleaned = hex_str.strip_prefix("0x").unwrap_or(hex_str); + let val = BurnchainHeaderHash::from_hex(cleaned).map_err(DeError::custom)?; + let ser_val = serde_json::to_value(val).map_err(DeError::custom)?; + map.insert("burn_header_hash".to_string(), ser_val); + } + + if let Some(val) = map.remove("burn_txid") { + map.insert("txid".to_string(), val); + } + if let Some(val) = map.remove("burn_block_height") { + map.insert("block_height".to_string(), val); + } + Ok(()) +} + impl BlockstackOperationType { pub fn opcode(&self) -> Opcodes { match *self { @@ -475,6 +602,114 @@ impl BlockstackOperationType { }; } + // Replace all the normalize_* functions with minimal implementations + fn normalize_pre_stx_fields<'de, D>( + map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields::(map)?; + if let Some(serde_json::Value::Object(obj)) = map.get_mut("output") { + normalize_stacks_addr_fields::(obj)?; + } + Ok(()) + } + + fn normalize_stack_stx_fields<'de, D>( + map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields::(map)?; + if let Some(serde_json::Value::Object(obj)) = map.get_mut("sender") { + normalize_stacks_addr_fields::(obj)?; + } + if let Some(reward_val) = map.get("reward_addr") { + let b58_str = reward_val + .as_str() + .ok_or_else(|| DeError::custom("Expected base58 string in reward_addr"))?; + let addr = PoxAddress::from_b58(b58_str) + .ok_or_else(|| DeError::custom("Invalid stacks address"))?; + let val = serde_json::to_value(addr).map_err(DeError::custom)?; + map.insert("reward_addr".into(), val); + } + Ok(()) + } + + fn normalize_transfer_stx_fields<'de, D>( + map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields::(map)?; + for field in ["recipient", "sender"] { + if let Some(serde_json::Value::Object(obj)) = map.get_mut(field) { + normalize_stacks_addr_fields::(obj)?; + } + } + if let Some(memo_str) = map.get("memo").and_then(serde_json::Value::as_str) { + let memo_hex = memo_str.trim_start_matches("0x"); + let memo_bytes = hex_bytes(memo_hex).map_err(DeError::custom)?; + let val = serde_json::to_value(memo_bytes).map_err(DeError::custom)?; + map.insert("memo".into(), val); + } + Ok(()) + } + + fn normalize_delegate_stx_fields<'de, D>( + map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields::(map)?; + if let Some(serde_json::Value::Array(arr)) = map.get("reward_addr") { + if arr.len() == 2 { + let index = arr[0] + .as_u64() + .ok_or_else(|| DeError::custom("Expected u64 index"))? + as u32; + let b58_str = arr[1] + .as_str() + .ok_or_else(|| DeError::custom("Expected base58 string"))?; + let addr = PoxAddress::from_b58(b58_str) + .ok_or_else(|| DeError::custom("Invalid stacks address"))?; + let val = serde_json::to_value((index, addr)).map_err(DeError::custom)?; + map.insert("reward_addr".into(), val); + } + } + for field in ["delegate_to", "sender"] { + if let Some(serde_json::Value::Object(obj)) = map.get_mut(field) { + normalize_stacks_addr_fields::(obj)?; + } + } + Ok(()) + } + + fn normalize_vote_for_aggregate_key_fields<'de, D>( + map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields::(map)?; + for field in ["aggregate_key", "signer_key"] { + if let Some(hex_str) = map.get(field).and_then(serde_json::Value::as_str) { + let cleaned = hex_str.strip_prefix("0x").unwrap_or(hex_str); + let val = StacksPublicKeyBuffer::from_hex(cleaned).map_err(DeError::custom)?; + let ser_val = serde_json::to_value(val).map_err(DeError::custom)?; + map.insert(field.to_string(), ser_val); + } + } + if let Some(serde_json::Value::Object(obj)) = map.get_mut("sender") { + normalize_stacks_addr_fields::(obj)?; + } + Ok(()) + } + pub fn pre_stx_to_json(op: &PreStxOp) -> serde_json::Value { json!({ "pre_stx": { diff --git a/stackslib/src/chainstate/burn/operations/test/mod.rs b/stackslib/src/chainstate/burn/operations/test/mod.rs index a27afaffd0..8524dcd94d 100644 --- a/stackslib/src/chainstate/burn/operations/test/mod.rs +++ b/stackslib/src/chainstate/burn/operations/test/mod.rs @@ -1,5 +1,11 @@ +use clarity::types::chainstate::{ + BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, StacksAddress, StacksPublicKey, VRFSeed, +}; +use clarity::types::StacksPublicKeyBuffer; +use clarity::util::vrf::{VRFPrivateKey, VRFPublicKey}; use rand::rngs::StdRng; use rand::SeedableRng; +use stacks_common::address::AddressHashMode; use stacks_common::util::hash::Hash160; use crate::burnchains::bitcoin::address::{ @@ -9,8 +15,14 @@ use crate::burnchains::bitcoin::{ BitcoinInputType, BitcoinNetworkType, BitcoinTransaction, BitcoinTxInputStructured, BitcoinTxOutput, }; -use crate::burnchains::{BurnchainBlockHeader, BurnchainTransaction, Txid}; +use crate::burnchains::{BurnchainBlockHeader, BurnchainSigner, BurnchainTransaction, Txid}; +use crate::chainstate::burn::operations::{ + blockstack_op_extended_deserialize, blockstack_op_extended_serialize_opt, + BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, + StackStxOp, TransferStxOp, VoteForAggregateKeyOp, +}; use crate::chainstate::burn::Opcodes; +use crate::chainstate::stacks::address::PoxAddress; mod serialization; @@ -85,3 +97,169 @@ impl Output { } } } + +#[test] +fn serde_blockstack_ops() { + #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] + struct TestOpHolder { + #[serde( + serialize_with = "blockstack_op_extended_serialize_opt", + deserialize_with = "blockstack_op_extended_deserialize" + )] + burnchain_op: Option, + } + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::PreStx(PreStxOp { + output: StacksAddress::new(0, Hash160([2u8; 20])) + .expect("Unable to create StacksAddress"), + txid: Txid([3u8; 32]), + vtxindex: 1, + block_height: 20, + burn_header_hash: BurnchainHeaderHash([4u8; 32]), + })), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + + let deserialized: TestOpHolder = + serde_json::from_str(&json_str).expect("Failed to deserialize PreStxOp"); + assert_eq!(holder, deserialized); + + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::DelegateStx(DelegateStxOp { + sender: StacksAddress::new(0, Hash160([2u8; 20])) + .expect("Unable to create StacksAddress"), + delegate_to: StacksAddress::new(1, Hash160([10u8; 20])) + .expect("Unable ot create StacksAddress"), + reward_addr: Some(( + 30, + PoxAddress::Standard(StacksAddress::new(22, Hash160([0x01; 20])).unwrap(), None), + )), + delegated_ustx: 200, + until_burn_height: None, + txid: Txid([3u8; 32]), + vtxindex: 1, + block_height: 20, + burn_header_hash: BurnchainHeaderHash([4u8; 32]), + })), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + + let deserialized: TestOpHolder = + serde_json::from_str(&json_str).expect("Failed to deserialize DelegateStxOp"); + assert_eq!(holder, deserialized); + + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::StackStx(StackStxOp { + sender: StacksAddress::new(0, Hash160([2u8; 20])) + .expect("Unable to create StacksAddress"), + reward_addr: PoxAddress::Standard( + StacksAddress::new(22, Hash160([0x01; 20])).unwrap(), + None, + ), + stacked_ustx: 42, + num_cycles: 3, + max_amount: None, + signer_key: None, + auth_id: None, + txid: Txid([3u8; 32]), + vtxindex: 1, + block_height: 20, + burn_header_hash: BurnchainHeaderHash([4u8; 32]), + })), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + + let deserialized: TestOpHolder = + serde_json::from_str(&json_str).expect("Failed to deserialize json value into StackStxOp"); + assert_eq!(holder, deserialized); + + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::TransferStx(TransferStxOp { + sender: StacksAddress::new(0, Hash160([2u8; 20])) + .expect("Unable to create StacksAddress"), + recipient: StacksAddress::new(0, Hash160([6u8; 20])) + .expect("Unable to create StacksAddress"), + transfered_ustx: 20, + memo: vec![], + txid: Txid([3u8; 32]), + vtxindex: 1, + block_height: 20, + burn_header_hash: BurnchainHeaderHash([4u8; 32]), + })), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + + let deserialized: TestOpHolder = serde_json::from_str(&json_str) + .expect("Failed to deserialize json value into TransferStxOp"); + assert_eq!(holder, deserialized); + + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::VoteForAggregateKey( + VoteForAggregateKeyOp { + sender: StacksAddress::new(0, Hash160([2u8; 20])) + .expect("Unable to create StacksAddress"), + aggregate_key: StacksPublicKeyBuffer([3u8; 33]), + round: 10, + signer_index: 11, + reward_cycle: 2, + signer_key: StacksPublicKeyBuffer([2u8; 33]), + txid: Txid([3u8; 32]), + vtxindex: 1, + block_height: 20, + burn_header_hash: BurnchainHeaderHash([4u8; 32]), + }, + )), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + + let deserialized: TestOpHolder = serde_json::from_str(&json_str) + .expect("Failed to deserialize json value into VoteForAggregateKeyOp"); + assert_eq!(holder, deserialized); + + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::LeaderBlockCommit( + LeaderBlockCommitOp { + block_header_hash: BlockHeaderHash([8u8; 32]), + new_seed: VRFSeed([12u8; 32]), + txid: Txid([3u8; 32]), + parent_block_ptr: 1, + parent_vtxindex: 2, + key_block_ptr: 3, + key_vtxindex: 4, + memo: vec![], + burn_fee: 5, + vtxindex: 1, + input: (Txid([1u8; 32]), 1), + block_height: 20, + burn_parent_modulus: 6, + apparent_sender: BurnchainSigner("Hello there".into()), + commit_outs: vec![], + treatment: vec![], + sunset_burn: 6, + burn_header_hash: BurnchainHeaderHash([4u8; 32]), + }, + )), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + let deserialized: TestOpHolder = serde_json::from_str(&json_str) + .expect("Failed to deserialize json value into LeaderBlockCommitOp"); + assert!(deserialized.burnchain_op.is_none()); + + let holder = TestOpHolder { + burnchain_op: Some(BlockstackOperationType::LeaderKeyRegister( + LeaderKeyRegisterOp { + consensus_hash: ConsensusHash([0u8; 20]), + public_key: VRFPublicKey::from_private(&VRFPrivateKey::new()), + memo: vec![], + txid: Txid([3u8; 32]), + vtxindex: 0, + block_height: 1, + burn_header_hash: BurnchainHeaderHash([9u8; 32]), + }, + )), + }; + let json_str = serde_json::to_string_pretty(&holder).expect("Failed to convert to json string"); + let deserialized: TestOpHolder = serde_json::from_str(&json_str) + .expect("Failed to deserialize json value into LeaderBlockCommitOp"); + assert!(deserialized.burnchain_op.is_none()); +} diff --git a/stackslib/src/net/api/mod.rs b/stackslib/src/net/api/mod.rs index c26333c052..45fbdda42f 100644 --- a/stackslib/src/net/api/mod.rs +++ b/stackslib/src/net/api/mod.rs @@ -225,6 +225,37 @@ pub mod prefix_hex { } } +/// This module serde encode and decodes structs that +/// implement StacksMessageCodec as a 0x-prefixed hex string. +pub mod prefix_hex_codec { + use clarity::codec::StacksMessageCodec; + use clarity::util::hash::{hex_bytes, to_hex}; + + pub fn serialize( + val: &T, + s: S, + ) -> Result { + let mut bytes = vec![]; + val.consensus_serialize(&mut bytes) + .map_err(serde::ser::Error::custom)?; + s.serialize_str(&format!("0x{}", to_hex(&bytes))) + } + + pub fn deserialize<'de, D: serde::Deserializer<'de>, T: StacksMessageCodec>( + d: D, + ) -> Result { + let inst_str: String = serde::Deserialize::deserialize(d)?; + let Some(hex_str) = inst_str.get(2..) else { + return Err(serde::de::Error::invalid_length( + inst_str.len(), + &"at least length 2 string", + )); + }; + let bytes = hex_bytes(hex_str).map_err(serde::de::Error::custom)?; + T::consensus_deserialize(&mut &bytes[..]).map_err(serde::de::Error::custom) + } +} + pub trait HexDeser: Sized { fn try_from(hex: &str) -> Result; } @@ -247,3 +278,4 @@ impl_hex_deser!(ConsensusHash); impl_hex_deser!(BlockHeaderHash); impl_hex_deser!(Hash160); impl_hex_deser!(Sha512Trunc256Sum); +impl_hex_deser!(Txid); diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index a142dc871e..c3a861e800 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -24,7 +24,9 @@ use std::sync::{Arc, Mutex}; use std::thread::sleep; use std::time::Duration; -use clarity::vm::analysis::contract_interface_builder::build_contract_interface; +use clarity::vm::analysis::contract_interface_builder::{ + build_contract_interface, ContractInterface, +}; use clarity::vm::costs::ExecutionCost; use clarity::vm::events::{FTEventType, NFTEventType, STXEventType}; use clarity::vm::types::{AssetIdentifier, QualifiedContractIdentifier, Value}; @@ -34,7 +36,10 @@ use rand::Rng; use rusqlite::{params, Connection}; use serde_json::json; use stacks::burnchains::{PoxConstants, Txid}; -use stacks::chainstate::burn::operations::BlockstackOperationType; +use stacks::chainstate::burn::operations::{ + blockstack_op_extended_deserialize, blockstack_op_extended_serialize_opt, + BlockstackOperationType, +}; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::coordinator::BlockEventDispatcher; use stacks::chainstate::nakamoto::NakamotoBlock; @@ -59,6 +64,7 @@ use stacks::libstackerdb::StackerDBChunkData; use stacks::net::api::postblock_proposal::{ BlockValidateOk, BlockValidateReject, BlockValidateResponse, }; +use stacks::net::api::{prefix_hex, prefix_hex_codec, prefix_opt_hex}; use stacks::net::atlas::{Attachment, AttachmentInstance}; use stacks::net::http::HttpRequestContents; use stacks::net::httpcore::{send_http_request, StacksHttpRequest}; @@ -95,15 +101,6 @@ pub struct EventObserver { pub disable_retries: bool, } -struct ReceiptPayloadInfo<'a> { - txid: String, - success: &'a str, - raw_result: String, - raw_tx: String, - contract_interface_json: serde_json::Value, - burnchain_op_json: serde_json::Value, -} - const STATUS_RESP_TRUE: &str = "success"; const STATUS_RESP_NOT_COMMITTED: &str = "abort_by_response"; const STATUS_RESP_POST_CONDITION: &str = "abort_by_post_condition"; @@ -334,6 +331,51 @@ impl RewardSetEventPayload { } } +pub fn hex_prefix_string( + hex_string: &String, + s: S, +) -> Result { + let prefixed = format!("0x{hex_string}"); + s.serialize_str(&prefixed) +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub struct TransactionEventPayload<'a> { + #[serde(with = "prefix_hex")] + /// The transaction id + pub txid: Txid, + /// The transaction index + pub tx_index: u32, + /// The transaction status + pub status: &'a str, + #[serde(with = "prefix_hex_codec")] + /// The raw transaction result + pub raw_result: Value, + /// The hex encoded raw transaction + #[serde(serialize_with = "hex_prefix_string")] + pub raw_tx: String, + /// The contract interface + pub contract_interface: Option, + /// The burnchain op + #[serde( + serialize_with = "blockstack_op_extended_serialize_opt", + deserialize_with = "blockstack_op_extended_deserialize" + )] + pub burnchain_op: Option, + /// The transaction execution cost + pub execution_cost: ExecutionCost, + /// The microblock sequence + pub microblock_sequence: Option, + #[serde(with = "prefix_opt_hex")] + /// The microblock hash + pub microblock_hash: Option, + #[serde(with = "prefix_opt_hex")] + /// The microblock parent hash + pub microblock_parent_hash: Option, + /// Error information if one occurred in the Clarity VM + pub vm_error: Option, +} + #[cfg(test)] static TEST_EVENT_OBSERVER_SKIP_RETRY: LazyLock> = LazyLock::new(TestFlag::default); @@ -575,11 +617,14 @@ impl EventObserver { }) } - /// Returns tuple of (txid, success, raw_result, raw_tx, contract_interface_json) - fn generate_payload_info_for_receipt(receipt: &StacksTransactionReceipt) -> ReceiptPayloadInfo { + /// Returns transaction event payload to send for new block or microblock event + fn make_new_block_txs_payload( + receipt: &StacksTransactionReceipt, + tx_index: u32, + ) -> TransactionEventPayload { let tx = &receipt.transaction; - let success = match (receipt.post_condition_aborted, &receipt.result) { + let status = match (receipt.post_condition_aborted, &receipt.result) { (false, Value::Response(response_data)) => { if response_data.committed { STATUS_RESP_TRUE @@ -589,77 +634,47 @@ impl EventObserver { } (true, Value::Response(_)) => STATUS_RESP_POST_CONDITION, _ => { - if let TransactionOrigin::Stacks(inner_tx) = &tx { - if let TransactionPayload::PoisonMicroblock(..) = &inner_tx.payload { - STATUS_RESP_TRUE - } else { - unreachable!() // Transaction results should otherwise always be a Value::Response type - } - } else { - unreachable!() // Transaction results should always be a Value::Response type + if !matches!( + tx, + TransactionOrigin::Stacks(StacksTransaction { + payload: TransactionPayload::PoisonMicroblock(_, _), + .. + }) + ) { + unreachable!("Unexpected transaction result type"); } + STATUS_RESP_TRUE } }; - let (txid, raw_tx, burnchain_op_json) = match tx { - TransactionOrigin::Burn(op) => ( - op.txid().to_string(), - "00".to_string(), - BlockstackOperationType::blockstack_op_to_json(op), - ), + let (txid, raw_tx, burnchain_op) = match tx { + TransactionOrigin::Burn(op) => (op.txid(), "00".to_string(), Some(op.clone())), TransactionOrigin::Stacks(ref tx) => { - let txid = tx.txid().to_string(); - let bytes = tx.serialize_to_vec(); - (txid, bytes_to_hex(&bytes), json!(null)) + let txid = tx.txid(); + let bytes = bytes_to_hex(&tx.serialize_to_vec()); + (txid, bytes, None) } }; - let raw_result = { - let bytes = receipt - .result - .serialize_to_vec() - .expect("FATAL: failed to serialize transaction receipt"); - bytes_to_hex(&bytes) - }; - let contract_interface_json = { - match &receipt.contract_analysis { - Some(analysis) => json!(build_contract_interface(analysis) - .expect("FATAL: failed to serialize contract publish receipt")), - None => json!(null), - } - }; - ReceiptPayloadInfo { + TransactionEventPayload { txid, - success, - raw_result, + tx_index, + status, + raw_result: receipt.result.clone(), raw_tx, - contract_interface_json, - burnchain_op_json, + contract_interface: receipt.contract_analysis.as_ref().map(|analysis| { + build_contract_interface(analysis) + .expect("FATAL: failed to serialize contract publish receipt") + }), + burnchain_op, + execution_cost: receipt.execution_cost.clone(), + microblock_sequence: receipt.microblock_header.as_ref().map(|x| x.sequence), + microblock_hash: receipt.microblock_header.as_ref().map(|x| x.block_hash()), + microblock_parent_hash: receipt.microblock_header.as_ref().map(|x| x.prev_block), + vm_error: receipt.vm_error.clone(), } } - /// Returns json payload to send for new block or microblock event - fn make_new_block_txs_payload( - receipt: &StacksTransactionReceipt, - tx_index: u32, - ) -> serde_json::Value { - let receipt_payload_info = EventObserver::generate_payload_info_for_receipt(receipt); - - json!({ - "txid": format!("0x{}", &receipt_payload_info.txid), - "tx_index": tx_index, - "status": receipt_payload_info.success, - "raw_result": format!("0x{}", &receipt_payload_info.raw_result), - "raw_tx": format!("0x{}", &receipt_payload_info.raw_tx), - "contract_abi": receipt_payload_info.contract_interface_json, - "burnchain_op": receipt_payload_info.burnchain_op_json, - "execution_cost": receipt.execution_cost, - "microblock_sequence": receipt.microblock_header.as_ref().map(|x| x.sequence), - "microblock_hash": receipt.microblock_header.as_ref().map(|x| format!("0x{}", x.block_hash())), - "microblock_parent_hash": receipt.microblock_header.as_ref().map(|x| format!("0x{}", x.prev_block)), - }) - } - fn make_new_attachment_payload( attachment: &(AttachmentInstance, Attachment), ) -> serde_json::Value { @@ -688,7 +703,7 @@ impl EventObserver { &self, parent_index_block_hash: StacksBlockId, filtered_events: Vec<(usize, &(bool, Txid, &StacksTransactionEvent))>, - serialized_txs: &Vec, + serialized_txs: &Vec, burn_block_hash: BurnchainHeaderHash, burn_block_height: u32, burn_block_timestamp: u64, @@ -1847,14 +1862,27 @@ mod test { use std::thread; use std::time::Instant; + use clarity::boot_util::boot_code_id; use clarity::vm::costs::ExecutionCost; + use clarity::vm::events::SmartContractEventData; + use clarity::vm::types::StacksAddressExtensions; use serial_test::serial; + use stacks::address::{AddressHashMode, C32_ADDRESS_VERSION_TESTNET_SINGLESIG}; use stacks::burnchains::{PoxConstants, Txid}; + use stacks::chainstate::burn::operations::PreStxOp; use stacks::chainstate::nakamoto::{NakamotoBlock, NakamotoBlockHeader}; use stacks::chainstate::stacks::db::{StacksBlockHeaderTypes, StacksHeaderInfo}; use stacks::chainstate::stacks::events::StacksBlockEventData; - use stacks::chainstate::stacks::StacksBlock; - use stacks::types::chainstate::BlockHeaderHash; + use stacks::chainstate::stacks::{ + SinglesigHashMode, SinglesigSpendingCondition, StacksBlock, TenureChangeCause, + TenureChangePayload, TokenTransferMemo, TransactionAnchorMode, TransactionAuth, + TransactionPostConditionMode, TransactionPublicKeyEncoding, TransactionSpendingCondition, + TransactionVersion, + }; + use stacks::types::chainstate::{ + BlockHeaderHash, StacksAddress, StacksPrivateKey, StacksPublicKey, + }; + use stacks::util::hash::Hash160; use stacks::util::secp256k1::MessageSignature; use stacks_common::bitvec::BitVec; use stacks_common::types::chainstate::{BurnchainHeaderHash, StacksBlockId}; @@ -2665,4 +2693,172 @@ mod test { assert_eq!(event_dispatcher.registered_observers.len(), 1); } + + #[test] + /// This test checks that tx payloads properly convert the stacks transaction receipt regardless of the presence of the vm_error + fn make_new_block_txs_payload_vm_error() { + let privkey = StacksPrivateKey::random(); + let pubkey = StacksPublicKey::from_private(&privkey); + let addr = StacksAddress::from_public_keys( + C32_ADDRESS_VERSION_TESTNET_SINGLESIG, + &AddressHashMode::SerializeP2PKH, + 1, + &vec![pubkey], + ) + .unwrap(); + + let tx = StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: 0x80000000, + auth: TransactionAuth::from_p2pkh(&privkey).unwrap(), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::TokenTransfer( + addr.to_account_principal(), + 123, + TokenTransferMemo([0u8; 34]), + ), + }; + + let mut receipt = StacksTransactionReceipt { + transaction: TransactionOrigin::Burn(BlockstackOperationType::PreStx(PreStxOp { + output: StacksAddress::new(0, Hash160([1; 20])).unwrap(), + txid: tx.txid(), + vtxindex: 0, + block_height: 1, + burn_header_hash: BurnchainHeaderHash([5u8; 32]), + })), + events: vec![], + post_condition_aborted: true, + result: Value::okay_true(), + contract_analysis: None, + execution_cost: ExecutionCost { + write_length: 0, + write_count: 0, + read_length: 0, + read_count: 0, + runtime: 0, + }, + microblock_header: None, + vm_error: None, + stx_burned: 0u128, + tx_index: 0, + }; + + let payload_no_error = EventObserver::make_new_block_txs_payload(&receipt, 0); + assert_eq!(payload_no_error.vm_error, receipt.vm_error); + + receipt.vm_error = Some("Inconceivable!".into()); + + let payload_with_error = EventObserver::make_new_block_txs_payload(&receipt, 0); + assert_eq!(payload_with_error.vm_error, receipt.vm_error); + } + + fn make_tenure_change_payload() -> TenureChangePayload { + TenureChangePayload { + tenure_consensus_hash: ConsensusHash([0; 20]), + prev_tenure_consensus_hash: ConsensusHash([0; 20]), + burn_view_consensus_hash: ConsensusHash([0; 20]), + previous_tenure_end: StacksBlockId([0; 32]), + previous_tenure_blocks: 1, + cause: TenureChangeCause::Extended, + pubkey_hash: Hash160([0; 20]), + } + } + + fn make_tenure_change_tx(payload: TenureChangePayload) -> StacksTransaction { + StacksTransaction { + version: TransactionVersion::Testnet, + chain_id: 1, + auth: TransactionAuth::Standard(TransactionSpendingCondition::Singlesig( + SinglesigSpendingCondition { + hash_mode: SinglesigHashMode::P2PKH, + signer: Hash160([0; 20]), + nonce: 0, + tx_fee: 0, + key_encoding: TransactionPublicKeyEncoding::Compressed, + signature: MessageSignature([0; 65]), + }, + )), + anchor_mode: TransactionAnchorMode::Any, + post_condition_mode: TransactionPostConditionMode::Allow, + post_conditions: vec![], + payload: TransactionPayload::TenureChange(payload), + } + } + + #[test] + fn backwards_compatibility_transaction_event_payload() { + let tx = make_tenure_change_tx(make_tenure_change_payload()); + let receipt = StacksTransactionReceipt { + transaction: TransactionOrigin::Burn(BlockstackOperationType::PreStx(PreStxOp { + output: StacksAddress::new(0, Hash160([1; 20])).unwrap(), + txid: tx.txid(), + vtxindex: 0, + block_height: 1, + burn_header_hash: BurnchainHeaderHash([5u8; 32]), + })), + events: vec![StacksTransactionEvent::SmartContractEvent( + SmartContractEventData { + key: (boot_code_id("some-contract", false), "some string".into()), + value: Value::Bool(false), + }, + )], + post_condition_aborted: false, + result: Value::okay_true(), + stx_burned: 100, + contract_analysis: None, + execution_cost: ExecutionCost { + write_length: 1, + write_count: 2, + read_length: 3, + read_count: 4, + runtime: 5, + }, + microblock_header: None, + tx_index: 1, + vm_error: None, + }; + let payload = EventObserver::make_new_block_txs_payload(&receipt, 0); + let new_serialized_data = serde_json::to_string_pretty(&payload).expect("Failed"); + let old_serialized_data = r#" + { + "burnchain_op": { + "pre_stx": { + "burn_block_height": 1, + "burn_header_hash": "0505050505050505050505050505050505050505050505050505050505050505", + "burn_txid": "ace70e63009a2c2d22c0f948b146d8a28df13a2900f3b5f3cc78b56459ffef05", + "output": { + "address": "S0G2081040G2081040G2081040G2081054GYN98", + "address_hash_bytes": "0x0101010101010101010101010101010101010101", + "address_version": 0 + }, + "vtxindex": 0 + } + }, + "contract_abi": null, + "execution_cost": { + "read_count": 4, + "read_length": 3, + "runtime": 5, + "write_count": 2, + "write_length": 1 + }, + "microblock_hash": null, + "microblock_parent_hash": null, + "microblock_sequence": null, + "raw_result": "0x0703", + "raw_tx": "0x00", + "status": "success", + "tx_index": 0, + "txid": "0xace70e63009a2c2d22c0f948b146d8a28df13a2900f3b5f3cc78b56459ffef05" + } + "#; + let new_value: TransactionEventPayload = serde_json::from_str(&new_serialized_data) + .expect("Failed to deserialize new data as TransactionEventPayload"); + let old_value: TransactionEventPayload = serde_json::from_str(&old_serialized_data) + .expect("Failed to deserialize old data as TransactionEventPayload"); + assert_eq!(new_value, old_value); + } }