From 1e55e1591ebb3afd73e68539ab5917a05b7ee966 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 18 Apr 2025 11:42:53 -0700 Subject: [PATCH 01/10] Create TransactionEventPayload struct and add vm_error to it Signed-off-by: Jacinta Ferrant --- .../src/chainstate/burn/operations/mod.rs | 18 ++- stackslib/src/net/api/mod.rs | 32 ++++ testnet/stacks-node/src/event_dispatcher.rs | 145 +++++++++--------- 3 files changed, 121 insertions(+), 74 deletions(-) diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index 3d032d4c8a..dad25a09ab 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -17,7 +17,7 @@ use std::{error, fmt, fs, io}; use clarity::vm::types::PrincipalData; -use serde::Deserialize; +use serde::{Deserialize, Serialize, Serializer}; use serde_json::json; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, StacksAddress, StacksBlockId, TrieHash, VRFSeed, @@ -374,6 +374,22 @@ pub fn stacks_addr_serialize(addr: &StacksAddress) -> serde_json::Value { }) } +/// Serialization function for serializing extended information within the BlockstackOperationType +/// that is not printed via the standard serde implenentation. Specifically serializes additional +/// StacksAddress information that is normally lost. +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(), + } +} + impl BlockstackOperationType { pub fn opcode(&self) -> Opcodes { match *self { 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 d4c175ae01..0606d4fcc9 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,9 @@ 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_serialize_opt, BlockstackOperationType, +}; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::coordinator::BlockEventDispatcher; use stacks::chainstate::nakamoto::NakamotoBlock; @@ -59,6 +63,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 +100,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 +330,39 @@ impl RewardSetEventPayload { } } +#[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 + pub raw_tx: String, + /// The contract interface + pub contract_interface: Option, + /// The burnchain op + #[serde(serialize_with = "blockstack_op_extended_serialize_opt")] + 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); @@ -573,11 +602,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 @@ -587,77 +619,44 @@ 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 + let TransactionOrigin::Stacks(inner_tx) = tx else { + unreachable!("Transaction results should always be a Value::Response type"); + }; + if !matches!(inner_tx.payload, TransactionPayload::PoisonMicroblock(..)) { + unreachable!("Transaction results should always be a Value::Response 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(), "0x00".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, format!("0x{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 { @@ -686,7 +685,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, From 8d0377244366713f0fba4a23146839214b1d92fa Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 18 Apr 2025 11:49:18 -0700 Subject: [PATCH 02/10] Add a comment to raw_tx to indicate it already has been 0x prefixed Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/event_dispatcher.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 0606d4fcc9..84def9247f 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -342,7 +342,7 @@ pub struct TransactionEventPayload<'a> { #[serde(with = "prefix_hex_codec")] /// The raw transaction result pub raw_result: Value, - /// The hex encoded raw transaction + /// The 0x prefixed, hex encoded raw transaction pub raw_tx: String, /// The contract interface pub contract_interface: Option, @@ -644,10 +644,7 @@ impl EventObserver { status, raw_result: receipt.result.clone(), raw_tx, - contract_interface: receipt.contract_analysis.as_ref().map(|analysis| { - build_contract_interface(analysis) - .expect("FATAL: failed to serialize contract publish receipt") - }), + 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), From 7238d1d6e88b4d08f65b781ccfe86d2d810ddc29 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 18 Apr 2025 12:56:47 -0700 Subject: [PATCH 03/10] Fix clippy Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/event_dispatcher.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 84def9247f..aa66ac1c92 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -644,7 +644,10 @@ impl EventObserver { status, raw_result: receipt.result.clone(), raw_tx, - contract_interface: receipt.contract_analysis.as_ref().map(|analysis| build_contract_interface(analysis).expect("FATAL: failed to serialize contract publish receipt")), + 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), From 0d7be0171ba94335d12ee30b7d3db061396c37f3 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 18 Apr 2025 12:59:56 -0700 Subject: [PATCH 04/10] Fix typo in comment Signed-off-by: Jacinta Ferrant --- stackslib/src/chainstate/burn/operations/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index dad25a09ab..cff7fc27a7 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -375,8 +375,8 @@ pub fn stacks_addr_serialize(addr: &StacksAddress) -> serde_json::Value { } /// Serialization function for serializing extended information within the BlockstackOperationType -/// that is not printed via the standard serde implenentation. Specifically serializes additional -/// StacksAddress information that is normally lost. +/// 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, From 01922ea289b35541e92e6e4bfc7f655044ac7581 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 18 Apr 2025 13:45:19 -0700 Subject: [PATCH 05/10] Update changelog Signed-off-by: Jacinta Ferrant --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24fb73bacd..f0e48f56fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to the versioning scheme outlined in the [README.md](README.md). +## [Unreleased] + +### Added + +- Added field `vm_error` to EventObserver transaction output + ## [3.1.0.0.8] ### Added From 707c09a5de41fc5c0c84c387b184a4bcfef2d258 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 22 Apr 2025 16:16:52 -0700 Subject: [PATCH 06/10] CRC: add unit tests for converting vm_error Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/event_dispatcher.rs | 92 +++++++++++++++++++-- 1 file changed, 85 insertions(+), 7 deletions(-) diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index aa66ac1c92..bf0fcedaf4 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -619,11 +619,14 @@ impl EventObserver { } (true, Value::Response(_)) => STATUS_RESP_POST_CONDITION, _ => { - let TransactionOrigin::Stacks(inner_tx) = tx else { - unreachable!("Transaction results should always be a Value::Response type"); - }; - if !matches!(inner_tx.payload, TransactionPayload::PoisonMicroblock(..)) { - unreachable!("Transaction results should always be a Value::Response type"); + if !matches!( + tx, + TransactionOrigin::Stacks(StacksTransaction { + payload: TransactionPayload::PoisonMicroblock(_, _), + .. + }) + ) { + unreachable!("Unexpexted transaction result type"); } STATUS_RESP_TRUE } @@ -1841,13 +1844,21 @@ mod test { use std::time::Instant; use clarity::vm::costs::ExecutionCost; + 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::TransferStxOp; 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::{ + StacksBlock, TokenTransferMemo, TransactionAnchorMode, TransactionAuth, + TransactionPostConditionMode, TransactionVersion, + }; + use stacks::types::chainstate::{ + BlockHeaderHash, StacksAddress, StacksPrivateKey, StacksPublicKey, + }; use stacks::util::secp256k1::MessageSignature; use stacks_common::bitvec::BitVec; use stacks_common::types::chainstate::{BurnchainHeaderHash, StacksBlockId}; @@ -2658,4 +2669,71 @@ 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 txid = tx.txid(); + + let mut receipt = StacksTransactionReceipt { + transaction: TransactionOrigin::Burn(BlockstackOperationType::TransferStx( + TransferStxOp { + sender: addr, + recipient: addr, + memo: vec![], + transfered_ustx: 123, + txid, + vtxindex: 0, + block_height: 0, + burn_header_hash: BurnchainHeaderHash([0u8; 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); + } } From 5979c9dd9ad7df8e3512c585d0566108d3b0c394 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Fri, 25 Apr 2025 09:33:35 -0700 Subject: [PATCH 07/10] CRC: typo in unreachable message Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/event_dispatcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index bf0fcedaf4..0d13a89711 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -626,7 +626,7 @@ impl EventObserver { .. }) ) { - unreachable!("Unexpexted transaction result type"); + unreachable!("Unexpected transaction result type"); } STATUS_RESP_TRUE } From cac00ccf3b7905c07abcf24f3da26babebf3c7d0 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Apr 2025 17:47:22 -0700 Subject: [PATCH 08/10] Add deserializer for custom blockstack op fields and make sure backwards compatibility satisfied for TransactionEventPayload Signed-off-by: Jacinta Ferrant --- .../src/chainstate/burn/operations/mod.rs | 232 +++++++++++++++++- .../chainstate/burn/operations/test/mod.rs | 180 +++++++++++++- testnet/stacks-node/src/event_dispatcher.rs | 164 +++++++++++-- 3 files changed, 553 insertions(+), 23 deletions(-) diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index cff7fc27a7..e973213c41 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, Serialize, Serializer}; +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,32 @@ 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. @@ -390,6 +417,101 @@ pub fn blockstack_op_extended_serialize_opt( } } +/// Deserialize the burnchain op that was serialized with blockstack_op_to_json +pub fn deserialize_extended_blockstack_op<'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) +} + +macro_rules! normalize_common_fields { + ($map:ident, $de:ident) => {{ + normalize_hex_field::<$de, _>(&mut $map, "burn_header_hash", |s| { + BurnchainHeaderHash::from_hex(s).map_err(DeError::custom) + })?; + rename_field(&mut $map, "burn_txid", "txid"); + rename_field(&mut $map, "burn_block_height", "block_height"); + }}; +} + +// Utility function to normalize a hex string to a BurnchainHeaderHash JSON value +fn normalize_hex_field<'de, D, T>( + map: &mut serde_json::Map, + field: &str, + from_hex: fn(&str) -> Result, +) -> Result<(), D::Error> +where + D: Deserializer<'de>, + T: serde::Serialize, +{ + 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 = 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); + } + Ok(()) +} + +// Normalize renamed field +fn rename_field(map: &mut serde_json::Map, from: &str, to: &str) { + if let Some(val) = map.remove(from) { + map.insert(to.to_string(), val); + } +} + impl BlockstackOperationType { pub fn opcode(&self) -> Opcodes { match *self { @@ -491,6 +613,114 @@ impl BlockstackOperationType { }; } + // Replace all the normalize_* functions with minimal implementations + fn normalize_pre_stx_fields<'de, D>( + mut map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields!(map, D); + 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>( + mut map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields!(map, D); + 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>( + mut map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields!(map, D); + 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>( + mut map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields!(map, D); + 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>( + mut map: &mut serde_json::Map, + ) -> Result<(), D::Error> + where + D: Deserializer<'de>, + { + normalize_common_fields!(map, D); + 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..71b68bfc1f 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_serialize_opt, deserialize_extended_blockstack_op, + 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 = "deserialize_extended_blockstack_op" + )] + 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/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 0d13a89711..03e2564c76 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -37,7 +37,8 @@ use rusqlite::{params, Connection}; use serde_json::json; use stacks::burnchains::{PoxConstants, Txid}; use stacks::chainstate::burn::operations::{ - blockstack_op_extended_serialize_opt, BlockstackOperationType, + blockstack_op_extended_serialize_opt, deserialize_extended_blockstack_op, + BlockstackOperationType, }; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::coordinator::BlockEventDispatcher; @@ -330,6 +331,14 @@ 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")] @@ -342,12 +351,16 @@ pub struct TransactionEventPayload<'a> { #[serde(with = "prefix_hex_codec")] /// The raw transaction result pub raw_result: Value, - /// The 0x prefixed, hex encoded raw transaction + /// 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")] + #[serde( + serialize_with = "blockstack_op_extended_serialize_opt", + deserialize_with = "deserialize_extended_blockstack_op" + )] pub burnchain_op: Option, /// The transaction execution cost pub execution_cost: ExecutionCost, @@ -633,11 +646,11 @@ impl EventObserver { }; let (txid, raw_tx, burnchain_op) = match tx { - TransactionOrigin::Burn(op) => (op.txid(), "0x00".to_string(), Some(op.clone())), + TransactionOrigin::Burn(op) => (op.txid(), "00".to_string(), Some(op.clone())), TransactionOrigin::Stacks(ref tx) => { let txid = tx.txid(); let bytes = bytes_to_hex(&tx.serialize_to_vec()); - (txid, format!("0x{bytes}"), None) + (txid, bytes, None) } }; @@ -1843,22 +1856,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::TransferStxOp; + 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, TokenTransferMemo, TransactionAnchorMode, TransactionAuth, - TransactionPostConditionMode, TransactionVersion, + 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}; @@ -2696,21 +2714,15 @@ mod test { TokenTransferMemo([0u8; 34]), ), }; - let txid = tx.txid(); let mut receipt = StacksTransactionReceipt { - transaction: TransactionOrigin::Burn(BlockstackOperationType::TransferStx( - TransferStxOp { - sender: addr, - recipient: addr, - memo: vec![], - transfered_ustx: 123, - txid, - vtxindex: 0, - block_height: 0, - burn_header_hash: BurnchainHeaderHash([0u8; 32]), - }, - )), + 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(), @@ -2734,6 +2746,116 @@ mod test { receipt.vm_error = Some("Inconceivable!".into()); let payload_with_error = EventObserver::make_new_block_txs_payload(&receipt, 0); + let json = serde_json::to_string_pretty(&payload_with_error).unwrap(); + println!("PAYLOAD: {json}"); + assert!(false); 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); + } } From c0bde6aafdc2e98931188b6a466cec15e19a1935 Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Tue, 29 Apr 2025 17:56:18 -0700 Subject: [PATCH 09/10] Remove use of macro Signed-off-by: Jacinta Ferrant --- .../src/chainstate/burn/operations/mod.rs | 59 ++++++++----------- .../chainstate/burn/operations/test/mod.rs | 4 +- testnet/stacks-node/src/event_dispatcher.rs | 4 +- 3 files changed, 28 insertions(+), 39 deletions(-) diff --git a/stackslib/src/chainstate/burn/operations/mod.rs b/stackslib/src/chainstate/burn/operations/mod.rs index e973213c41..0eb0ab62b8 100644 --- a/stackslib/src/chainstate/burn/operations/mod.rs +++ b/stackslib/src/chainstate/burn/operations/mod.rs @@ -418,7 +418,7 @@ pub fn blockstack_op_extended_serialize_opt( } /// Deserialize the burnchain op that was serialized with blockstack_op_to_json -pub fn deserialize_extended_blockstack_op<'de, D>( +pub fn blockstack_op_extended_deserialize<'de, D>( deserializer: D, ) -> Result, D::Error> where @@ -476,40 +476,29 @@ where .map_err(serde::de::Error::custom) } -macro_rules! normalize_common_fields { - ($map:ident, $de:ident) => {{ - normalize_hex_field::<$de, _>(&mut $map, "burn_header_hash", |s| { - BurnchainHeaderHash::from_hex(s).map_err(DeError::custom) - })?; - rename_field(&mut $map, "burn_txid", "txid"); - rename_field(&mut $map, "burn_block_height", "block_height"); - }}; -} - -// Utility function to normalize a hex string to a BurnchainHeaderHash JSON value -fn normalize_hex_field<'de, D, T>( +fn normalize_common_fields<'de, D>( map: &mut serde_json::Map, - field: &str, - from_hex: fn(&str) -> Result, ) -> Result<(), D::Error> where D: Deserializer<'de>, - T: serde::Serialize, { - if let Some(hex_str) = map.get(field).and_then(serde_json::Value::as_str) { + 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 = from_hex(cleaned).map_err(DeError::custom)?; + let val = BurnchainHeaderHash::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); + map.insert("burn_header_hash".to_string(), ser_val); } - Ok(()) -} -// Normalize renamed field -fn rename_field(map: &mut serde_json::Map, from: &str, to: &str) { - if let Some(val) = map.remove(from) { - map.insert(to.to_string(), 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 { @@ -615,12 +604,12 @@ impl BlockstackOperationType { // Replace all the normalize_* functions with minimal implementations fn normalize_pre_stx_fields<'de, D>( - mut map: &mut serde_json::Map, + map: &mut serde_json::Map, ) -> Result<(), D::Error> where D: Deserializer<'de>, { - normalize_common_fields!(map, D); + normalize_common_fields::(map)?; if let Some(serde_json::Value::Object(obj)) = map.get_mut("output") { normalize_stacks_addr_fields::(obj)?; } @@ -628,12 +617,12 @@ impl BlockstackOperationType { } fn normalize_stack_stx_fields<'de, D>( - mut map: &mut serde_json::Map, + map: &mut serde_json::Map, ) -> Result<(), D::Error> where D: Deserializer<'de>, { - normalize_common_fields!(map, D); + normalize_common_fields::(map)?; if let Some(serde_json::Value::Object(obj)) = map.get_mut("sender") { normalize_stacks_addr_fields::(obj)?; } @@ -650,12 +639,12 @@ impl BlockstackOperationType { } fn normalize_transfer_stx_fields<'de, D>( - mut map: &mut serde_json::Map, + map: &mut serde_json::Map, ) -> Result<(), D::Error> where D: Deserializer<'de>, { - normalize_common_fields!(map, D); + 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)?; @@ -671,12 +660,12 @@ impl BlockstackOperationType { } fn normalize_delegate_stx_fields<'de, D>( - mut map: &mut serde_json::Map, + map: &mut serde_json::Map, ) -> Result<(), D::Error> where D: Deserializer<'de>, { - normalize_common_fields!(map, D); + normalize_common_fields::(map)?; if let Some(serde_json::Value::Array(arr)) = map.get("reward_addr") { if arr.len() == 2 { let index = arr[0] @@ -701,12 +690,12 @@ impl BlockstackOperationType { } fn normalize_vote_for_aggregate_key_fields<'de, D>( - mut map: &mut serde_json::Map, + map: &mut serde_json::Map, ) -> Result<(), D::Error> where D: Deserializer<'de>, { - normalize_common_fields!(map, D); + 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); diff --git a/stackslib/src/chainstate/burn/operations/test/mod.rs b/stackslib/src/chainstate/burn/operations/test/mod.rs index 71b68bfc1f..8524dcd94d 100644 --- a/stackslib/src/chainstate/burn/operations/test/mod.rs +++ b/stackslib/src/chainstate/burn/operations/test/mod.rs @@ -17,7 +17,7 @@ use crate::burnchains::bitcoin::{ }; use crate::burnchains::{BurnchainBlockHeader, BurnchainSigner, BurnchainTransaction, Txid}; use crate::chainstate::burn::operations::{ - blockstack_op_extended_serialize_opt, deserialize_extended_blockstack_op, + blockstack_op_extended_deserialize, blockstack_op_extended_serialize_opt, BlockstackOperationType, DelegateStxOp, LeaderBlockCommitOp, LeaderKeyRegisterOp, PreStxOp, StackStxOp, TransferStxOp, VoteForAggregateKeyOp, }; @@ -104,7 +104,7 @@ fn serde_blockstack_ops() { struct TestOpHolder { #[serde( serialize_with = "blockstack_op_extended_serialize_opt", - deserialize_with = "deserialize_extended_blockstack_op" + deserialize_with = "blockstack_op_extended_deserialize" )] burnchain_op: Option, } diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 03e2564c76..2339d27c10 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -37,7 +37,7 @@ use rusqlite::{params, Connection}; use serde_json::json; use stacks::burnchains::{PoxConstants, Txid}; use stacks::chainstate::burn::operations::{ - blockstack_op_extended_serialize_opt, deserialize_extended_blockstack_op, + blockstack_op_extended_deserialize, blockstack_op_extended_serialize_opt, BlockstackOperationType, }; use stacks::chainstate::burn::ConsensusHash; @@ -359,7 +359,7 @@ pub struct TransactionEventPayload<'a> { /// The burnchain op #[serde( serialize_with = "blockstack_op_extended_serialize_opt", - deserialize_with = "deserialize_extended_blockstack_op" + deserialize_with = "blockstack_op_extended_deserialize" )] pub burnchain_op: Option, /// The transaction execution cost From 39c54586099f4c129be6436e41fc7a0e5a27cbef Mon Sep 17 00:00:00 2001 From: Jacinta Ferrant Date: Wed, 30 Apr 2025 09:03:14 -0700 Subject: [PATCH 10/10] Remove testing assert from debugging Signed-off-by: Jacinta Ferrant --- testnet/stacks-node/src/event_dispatcher.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index 3872a93165..c3a861e800 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -2752,9 +2752,6 @@ mod test { receipt.vm_error = Some("Inconceivable!".into()); let payload_with_error = EventObserver::make_new_block_txs_payload(&receipt, 0); - let json = serde_json::to_string_pretty(&payload_with_error).unwrap(); - println!("PAYLOAD: {json}"); - assert!(false); assert_eq!(payload_with_error.vm_error, receipt.vm_error); }