From 8b8f2a077f50d94e5fe81036441077352978c5a5 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Fri, 17 Feb 2023 08:16:52 -0600 Subject: [PATCH 1/2] feat: integrate burnchain_op info event observer changes from stacks-blockchain --- src/chainstate/burn/operations/mod.rs | 360 ++++++++++++++++++-- src/chainstate/stacks/db/blocks.rs | 148 +------- src/chainstate/stacks/events.rs | 7 +- src/chainstate/stacks/miner.rs | 4 +- testnet/stacks-node/src/event_dispatcher.rs | 27 +- 5 files changed, 369 insertions(+), 177 deletions(-) diff --git a/src/chainstate/burn/operations/mod.rs b/src/chainstate/burn/operations/mod.rs index 982ed306fb..229177bf7f 100644 --- a/src/chainstate/burn/operations/mod.rs +++ b/src/chainstate/burn/operations/mod.rs @@ -42,6 +42,8 @@ use crate::chainstate::burn::Opcodes; use crate::util_lib::db::DBConn; use crate::util_lib::db::DBTx; use crate::util_lib::db::Error as db_error; +use clarity::util::HexError; +use serde::Deserialize; use stacks_common::util::hash::Hash160; use stacks_common::util::hash::Sha512Trunc256Sum; use stacks_common::util::secp256k1::MessageSignature; @@ -60,7 +62,6 @@ pub mod withdraw_nft; pub mod withdraw_stx; /// This module contains all burn-chain operations - #[derive(Debug)] pub enum Error { /// Failed to parse the operation from the burnchain transaction @@ -169,6 +170,70 @@ impl From for Error { } } +trait HexSerialization { + fn ser_to_hex(&self) -> String; + fn deser_from_hex(s: &str) -> Result; +} + +/// Implement HexSerialization for byte_array macro +/// defined types (i.e., types with to_hex and from_hex) +macro_rules! impl_hex_serialization { + ($thing:ident) => { + impl HexSerialization<$thing, HexError> for $thing { + fn ser_to_hex(&self) -> String { + self.to_hex() + } + fn deser_from_hex(s: &str) -> Result { + Self::from_hex(s) + } + } + }; +} + +impl_hex_serialization!(BurnchainHeaderHash); +impl_hex_serialization!(Txid); +impl_hex_serialization!(Sha512Trunc256Sum); + +fn hex_serialize, E: fmt::Display>( + bhh: &T, + s: S, +) -> Result { + let inst = bhh.ser_to_hex(); + s.serialize_str(inst.as_str()) +} + +fn hex_deserialize<'de, D: serde::Deserializer<'de>, T: HexSerialization, E: fmt::Display>( + d: D, +) -> Result { + let inst_str = String::deserialize(d)?; + T::deser_from_hex(&inst_str).map_err(serde::de::Error::custom) +} + +fn qc_serialize( + qc: &QualifiedContractIdentifier, + s: S, +) -> Result { + let inst = qc.to_string(); + s.serialize_str(inst.as_str()) +} + +fn qc_deserialize<'de, D: serde::Deserializer<'de>>( + d: D, +) -> Result { + let inst_str = String::deserialize(d)?; + QualifiedContractIdentifier::parse(&inst_str).map_err(serde::de::Error::custom) +} + +fn pd_serialize(pd: &PrincipalData, s: S) -> Result { + let inst = pd.to_string(); + s.serialize_str(inst.as_str()) +} + +fn pd_deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result { + let inst_str = String::deserialize(d)?; + PrincipalData::parse(&inst_str).map_err(serde::de::Error::custom) +} + #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct TransferStxOp { pub sender: StacksAddress, @@ -177,9 +242,10 @@ pub struct TransferStxOp { pub memo: Vec, // common to all transactions - pub txid: Txid, // transaction ID - pub vtxindex: u32, // index in the block where this tx occurs - pub block_height: u64, // block height at which this tx occurs + pub txid: Txid, // transaction ID + pub vtxindex: u32, // index in the block where this tx occurs + pub block_height: u64, // block height at which this tx occurs + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, // hash of the burn chain block header } @@ -216,108 +282,135 @@ pub struct LeaderBlockCommitOp { /// Hash of the committed block (anchor block hash) pub block_header_hash: BlockHeaderHash, /// Merkle root of the withdrawal events + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub withdrawal_merkle_root: Sha512Trunc256Sum, /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct DepositStxOp { /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, - // Amount of STX that was deposited + /// Amount of STX that was deposited pub amount: u128, - // The principal that performed the deposit + /// The principal that performed the deposit + #[serde(serialize_with = "pd_serialize", deserialize_with = "pd_deserialize")] pub sender: PrincipalData, } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct DepositFtOp { /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, - // Contract ID on L1 chain for this fungible token + /// Contract ID on L1 chain for this fungible token + #[serde(serialize_with = "qc_serialize", deserialize_with = "qc_deserialize")] pub l1_contract_id: QualifiedContractIdentifier, - // Contract ID on subnet for this fungible token + /// Contract ID on subnet for this fungible token + #[serde(serialize_with = "qc_serialize", deserialize_with = "qc_deserialize")] pub subnet_contract_id: QualifiedContractIdentifier, - // Name of the function to call in the subnet contract to execute deposit + /// Name of the function to call in the subnet contract to execute deposit pub subnet_function_name: ClarityName, - // Name of fungible token + /// Name of fungible token pub name: String, - // Amount of the fungible token that was deposited + /// Amount of the fungible token that was deposited pub amount: u128, - // The principal that performed the deposit + /// The principal that performed the deposit + #[serde(serialize_with = "pd_serialize", deserialize_with = "pd_deserialize")] pub sender: PrincipalData, } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct DepositNftOp { /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, - // Contract ID on L1 chain for this NFT + /// Contract ID on L1 chain for this NFT + #[serde(serialize_with = "qc_serialize", deserialize_with = "qc_deserialize")] pub l1_contract_id: QualifiedContractIdentifier, - // Contract ID on subnet for this NFT + /// Contract ID on subnet for this NFT + #[serde(serialize_with = "qc_serialize", deserialize_with = "qc_deserialize")] pub subnet_contract_id: QualifiedContractIdentifier, - // Name of the function to call in the subnet contract to execute deposit + /// Name of the function to call in the subnet contract to execute deposit pub subnet_function_name: ClarityName, - // The ID of the NFT transferred + /// The ID of the NFT transferred pub id: u128, - // The principal that performed the deposit + /// The principal that performed the deposit + #[serde(serialize_with = "pd_serialize", deserialize_with = "pd_deserialize")] pub sender: PrincipalData, } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct WithdrawStxOp { /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, - // Amount of STX that was withdrawn + /// Amount of STX that was withdrawn pub amount: u128, - // The principal that is the recipient of this withdrawal + /// The principal that is the recipient of this withdrawal + #[serde(serialize_with = "pd_serialize", deserialize_with = "pd_deserialize")] pub recipient: PrincipalData, } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct WithdrawFtOp { /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, // Contract ID on L1 chain for this fungible token + #[serde(serialize_with = "qc_serialize", deserialize_with = "qc_deserialize")] pub l1_contract_id: QualifiedContractIdentifier, - // The name of the fungible token + /// The name of the fungible token pub name: String, - // Amount of the fungible token that was withdrawn + /// Amount of the fungible token that was withdrawn pub amount: u128, - // The principal the contract is sending the fungible token to + /// The principal the contract is sending the fungible token to + #[serde(serialize_with = "pd_serialize", deserialize_with = "pd_deserialize")] pub recipient: PrincipalData, } #[derive(Debug, PartialEq, Clone, Eq, Serialize, Deserialize)] pub struct WithdrawNftOp { /// Transaction ID of this commit op + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub txid: Txid, /// Hash of the base chain block that produced this commit op. + #[serde(serialize_with = "hex_serialize", deserialize_with = "hex_deserialize")] pub burn_header_hash: BurnchainHeaderHash, - // Contract ID on L1 chain for this NFT + /// Contract ID on L1 chain for this NFT + #[serde(serialize_with = "qc_serialize", deserialize_with = "qc_deserialize")] pub l1_contract_id: QualifiedContractIdentifier, - // The ID of the NFT being withdrawn + /// The ID of the NFT being withdrawn pub id: u128, - // The principal the contract is sending the NFT to + /// The principal the contract is sending the NFT to + #[serde(serialize_with = "pd_serialize", deserialize_with = "pd_deserialize")] pub recipient: PrincipalData, } @@ -353,7 +446,8 @@ pub struct UserBurnSupportOp { pub burn_header_hash: BurnchainHeaderHash, // hash of burnchain block with this tx } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum BlockstackOperationType { LeaderBlockCommit(LeaderBlockCommitOp), DepositStx(DepositStxOp), @@ -472,6 +566,11 @@ impl BlockstackOperationType { BlockstackOperationType::WithdrawNft(ref mut data) => data.burn_header_hash = hash, }; } + + /// Explicit JSON serialization function for burnchain ops. + pub fn blockstack_op_to_json(&self) -> serde_json::Value { + serde_json::to_value(self).unwrap() + } } impl fmt::Display for BlockstackOperationType { @@ -500,3 +599,212 @@ pub fn parse_u32_from_be(bytes: &[u8]) -> Option { pub fn parse_u16_from_be(bytes: &[u8]) -> Option { bytes.try_into().ok().map(u16::from_be_bytes) } + +#[cfg(test)] +mod json_tests { + use super::*; + + #[test] + fn deposit_ft() { + let deposit_ft = DepositFtOp { + txid: Txid([0x11; 32]), + burn_header_hash: BurnchainHeaderHash([0xaa; 32]), + l1_contract_id: QualifiedContractIdentifier::parse("SP000000000000000000002Q6VF78.bns") + .unwrap(), + subnet_contract_id: QualifiedContractIdentifier::parse( + "SP000000000000000000002Q6VF78.bns", + ) + .unwrap(), + subnet_function_name: "deposit-mint".into(), + name: "ft-name".into(), + amount: 7381273163198273, + sender: PrincipalData::parse("SP000000000000000000002Q6VF78.bns").unwrap(), + } + .into(); + + let expected = r#" + { + "deposit_ft": { + "amount": 7381273163198273, + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "name": "ft-name", + "sender": "SP000000000000000000002Q6VF78.bns", + "subnet_contract_id": "SP000000000000000000002Q6VF78.bns", + "subnet_function_name": "deposit-mint", + "txid": "1111111111111111111111111111111111111111111111111111111111111111" + } + }"#; + + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&deposit_ft), + serde_json::from_str::(expected).unwrap() + ); + } + + #[test] + fn deposit_nft() { + let deposit_nft = DepositNftOp { + txid: Txid([0xf1; 32]), + burn_header_hash: BurnchainHeaderHash([0xcc; 32]), + l1_contract_id: QualifiedContractIdentifier::parse("SP000000000000000000002Q6VF78.bns") + .unwrap(), + subnet_contract_id: QualifiedContractIdentifier::parse( + "SP000000000000000000002Q6VF78.bns", + ) + .unwrap(), + subnet_function_name: "deposit-nft-mint".into(), + sender: PrincipalData::parse("SP000000000000000000002Q6VF78.bns").unwrap(), + id: 123123, + } + .into(); + + let expected = r#" + { + "deposit_nft": { + "burn_header_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "id": 123123, + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "sender": "SP000000000000000000002Q6VF78.bns", + "subnet_contract_id": "SP000000000000000000002Q6VF78.bns", + "subnet_function_name": "deposit-nft-mint", + "txid": "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + } + }"#; + + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&deposit_nft), + serde_json::from_str::(expected).unwrap() + ); + } + + #[test] + fn deposit_stx() { + let deposit_stx = DepositStxOp { + txid: Txid([0x33; 32]), + burn_header_hash: BurnchainHeaderHash([0xaa; 32]), + amount: 7381273163198273, + sender: PrincipalData::parse("SP000000000000000000002Q6VF78").unwrap(), + } + .into(); + let expected = r#" + { + "deposit_stx": { + "amount": 7381273163198273, + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sender": "SP000000000000000000002Q6VF78", + "txid": "3333333333333333333333333333333333333333333333333333333333333333" + } + }"#; + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&deposit_stx), + serde_json::from_str::(expected).unwrap() + ); + } + + #[test] + fn withdraw_ft() { + let withdraw_ft = WithdrawFtOp { + txid: Txid([0x11; 32]), + burn_header_hash: BurnchainHeaderHash([0xaa; 32]), + l1_contract_id: QualifiedContractIdentifier::parse("SP000000000000000000002Q6VF78.bns") + .unwrap(), + name: "ft-name".into(), + amount: 7381273163198273, + recipient: PrincipalData::parse("SP000000000000000000002Q6VF78.bns").unwrap(), + } + .into(); + let expected = r#" + { + "withdraw_ft": { + "amount": 7381273163198273, + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "name": "ft-name", + "recipient": "SP000000000000000000002Q6VF78.bns", + "txid": "1111111111111111111111111111111111111111111111111111111111111111" + } + }"#; + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&withdraw_ft), + serde_json::from_str::(expected).unwrap() + ); + } + + #[test] + fn withdraw_nft() { + let withdraw_nft = WithdrawNftOp { + txid: Txid([0xf1; 32]), + burn_header_hash: BurnchainHeaderHash([0xcc; 32]), + l1_contract_id: QualifiedContractIdentifier::parse("SP000000000000000000002Q6VF78.bns") + .unwrap(), + recipient: PrincipalData::parse("SP000000000000000000002Q6VF78.bns").unwrap(), + id: 123123, + } + .into(); + let expected = r#" + { + "withdraw_nft": { + "burn_header_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "id": 123123, + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "recipient": "SP000000000000000000002Q6VF78.bns", + "txid": "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + } + }"#; + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&withdraw_nft), + serde_json::from_str::(expected).unwrap() + ); + } + + #[test] + fn withdraw_stx() { + let withdraw_stx = WithdrawStxOp { + txid: Txid([0x3b; 32]), + burn_header_hash: BurnchainHeaderHash([0xba; 32]), + amount: 7381273163198273, + recipient: PrincipalData::parse("SP000000000000000000002Q6VF78").unwrap(), + } + .into(); + let expected = r#" + { + "withdraw_stx": { + "amount": 7381273163198273, + "burn_header_hash": "babababababababababababababababababababababababababababababababa", + "recipient": "SP000000000000000000002Q6VF78", + "txid": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" + } + }"#; + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&withdraw_stx), + serde_json::from_str::(expected).unwrap() + ); + } + + #[test] + fn commit_op() { + let commit_op = LeaderBlockCommitOp { + txid: Txid([0xa1; 32]), + burn_header_hash: BurnchainHeaderHash([0xaa; 32]), + block_header_hash: BlockHeaderHash([0x12; 32]), + withdrawal_merkle_root: Sha512Trunc256Sum([0x31; 32]), + } + .into(); + + let expected = r#" + { + "leader_block_commit": { + "block_header_hash": "1212121212121212121212121212121212121212121212121212121212121212", + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "withdrawal_merkle_root": "3131313131313131313131313131313131313131313131313131313131313131" + } + } + "#; + assert_eq!( + BlockstackOperationType::blockstack_op_to_json(&commit_op), + serde_json::from_str::(expected).unwrap() + ); + } +} diff --git a/src/chainstate/stacks/db/blocks.rs b/src/chainstate/stacks/db/blocks.rs index 782b167033..5e5bf01dad 100644 --- a/src/chainstate/stacks/db/blocks.rs +++ b/src/chainstate/stacks/db/blocks.rs @@ -4523,142 +4523,6 @@ impl StacksChainState { Ok((applied, receipts)) } - /// Process any Stacking-related bitcoin operations - /// that haven't been processed in this Stacks fork yet. - pub fn process_stacking_ops( - clarity_tx: &mut ClarityTx, - operations: Vec, - ) -> Vec { - let mut all_receipts = vec![]; - let mainnet = clarity_tx.config.mainnet; - let mut cost_so_far = clarity_tx.cost_so_far(); - for stack_stx_op in operations.into_iter() { - let StackStxOp { - sender, - reward_addr, - stacked_ustx, - num_cycles, - block_height, - txid, - burn_header_hash, - .. - } = stack_stx_op; - let result = clarity_tx.connection().as_transaction(|tx| { - tx.run_contract_call( - &sender.into(), - None, - &boot_code_id("pox", mainnet), - "stack-stx", - &[ - Value::UInt(stacked_ustx), - reward_addr.as_clarity_tuple().into(), - Value::UInt(u128::from(block_height)), - Value::UInt(u128::from(num_cycles)), - ], - |_, _| false, - ) - }); - match result { - Ok((value, _, events)) => { - if let Value::Response(ref resp) = value { - if !resp.committed { - debug!("StackStx burn op rejected by PoX contract."; - "txid" => %txid, - "burn_block" => %burn_header_hash, - "contract_call_ecode" => %resp.data); - } - let mut execution_cost = clarity_tx.cost_so_far(); - execution_cost - .sub(&cost_so_far) - .expect("BUG: cost declined between executions"); - cost_so_far = clarity_tx.cost_so_far(); - - let receipt = StacksTransactionReceipt { - transaction: TransactionOrigin::Burn(txid), - events, - result: value, - post_condition_aborted: false, - stx_burned: 0, - contract_analysis: None, - execution_cost, - microblock_header: None, - tx_index: 0, - }; - - all_receipts.push(receipt); - } else { - unreachable!( - "BUG: Non-response value returned by Stacking STX burnchain op" - ) - } - } - Err(e) => { - info!("StackStx burn op processing error."; - "error" => %format!("{:?}", e), - "txid" => %txid, - "burn_block" => %burn_header_hash); - } - }; - } - - all_receipts - } - - /// Process any STX transfer bitcoin operations - /// that haven't been processed in this Stacks fork yet. - pub fn process_transfer_ops( - clarity_tx: &mut ClarityTx, - mut operations: Vec, - ) -> Vec { - operations.sort_by_key(|op| op.vtxindex); - let (all_receipts, _) = - clarity_tx.with_temporary_cost_tracker(LimitedCostTracker::new_free(), |clarity_tx| { - operations - .into_iter() - .filter_map(|transfer_stx_op| { - let TransferStxOp { - sender, - recipient, - transfered_ustx, - txid, - burn_header_hash, - .. - } = transfer_stx_op; - let result = clarity_tx.connection().as_transaction(|tx| { - tx.run_stx_transfer( - &sender.into(), - &recipient.into(), - transfered_ustx, - &BuffData { data: vec![] }, - ) - }); - match result { - Ok((value, _, events)) => Some(StacksTransactionReceipt { - transaction: TransactionOrigin::Burn(txid), - events, - result: value, - post_condition_aborted: false, - stx_burned: 0, - contract_analysis: None, - execution_cost: ExecutionCost::zero(), - microblock_header: None, - tx_index: 0, - }), - Err(e) => { - info!("TransferStx burn op processing error."; - "error" => ?e, - "txid" => %txid, - "burn_block" => %burn_header_hash); - None - } - } - }) - .collect() - }); - - all_receipts - } - /// Process any deposit STX operations that haven't been processed in this /// subnet fork yet. pub fn process_deposit_stx_ops( @@ -4675,7 +4539,7 @@ impl StacksChainState { amount, sender, .. - } = deposit_stx_op; + } = deposit_stx_op.clone(); // call the corresponding deposit function in the subnet contract let result = clarity_tx.connection().as_transaction(|tx| { StacksChainState::account_credit(tx, &sender, amount as u64); @@ -4690,7 +4554,7 @@ impl StacksChainState { clarity_tx.increment_ustx_liquid_supply(amount); Some(StacksTransactionReceipt { - transaction: TransactionOrigin::Burn(txid), + transaction: TransactionOrigin::Burn(deposit_stx_op.into()), events: vec![result], result: Value::okay_true(), post_condition_aborted: false, @@ -4726,7 +4590,7 @@ impl StacksChainState { amount, sender, .. - } = deposit_ft_op; + } = deposit_ft_op.clone(); // call the corresponding deposit function in the subnet contract let result = clarity_tx.connection().as_transaction(|tx| { tx.run_contract_call( @@ -4745,7 +4609,7 @@ impl StacksChainState { match result { Ok((value, _, events)) => Some(StacksTransactionReceipt { - transaction: TransactionOrigin::Burn(txid), + transaction: TransactionOrigin::Burn(deposit_ft_op.into()), events, result: value, post_condition_aborted: false, @@ -4786,7 +4650,7 @@ impl StacksChainState { id, sender, .. - } = deposit_nft_op; + } = deposit_nft_op.clone(); let result = clarity_tx.connection().as_transaction(|tx| { tx.run_contract_call( &sender.clone(), @@ -4804,7 +4668,7 @@ impl StacksChainState { match result { Ok((value, _, events)) => Some(StacksTransactionReceipt { - transaction: TransactionOrigin::Burn(txid), + transaction: TransactionOrigin::Burn(deposit_nft_op.into()), events, result: value, post_condition_aborted: false, diff --git a/src/chainstate/stacks/events.rs b/src/chainstate/stacks/events.rs index adcd8455c7..cfced1826d 100644 --- a/src/chainstate/stacks/events.rs +++ b/src/chainstate/stacks/events.rs @@ -1,4 +1,5 @@ use crate::burnchains::Txid; +use crate::chainstate::burn::operations::BlockstackOperationType; use crate::chainstate::stacks::StacksMicroblockHeader; use crate::chainstate::stacks::StacksTransaction; use crate::codec::StacksMessageCodec; @@ -14,7 +15,7 @@ pub use clarity::vm::events::StacksTransactionEvent; #[derive(Debug, Clone, PartialEq)] pub enum TransactionOrigin { Stacks(StacksTransaction), - Burn(Txid), + Burn(BlockstackOperationType), } impl From for TransactionOrigin { @@ -26,13 +27,13 @@ impl From for TransactionOrigin { impl TransactionOrigin { pub fn txid(&self) -> Txid { match self { - TransactionOrigin::Burn(txid) => txid.clone(), + TransactionOrigin::Burn(tx) => tx.txid(), TransactionOrigin::Stacks(tx) => tx.txid(), } } pub fn serialize_to_vec(&self) -> Vec { match self { - TransactionOrigin::Burn(txid) => txid.as_bytes().to_vec(), + TransactionOrigin::Burn(tx) => tx.txid().as_bytes().to_vec(), TransactionOrigin::Stacks(tx) => tx.txid().as_bytes().to_vec(), } } diff --git a/src/chainstate/stacks/miner.rs b/src/chainstate/stacks/miner.rs index f7ceff13c5..2d394c9694 100644 --- a/src/chainstate/stacks/miner.rs +++ b/src/chainstate/stacks/miner.rs @@ -240,12 +240,12 @@ pub struct TransactionSkippedEvent { pub error: String, } -fn hex_serialize(txid: &Txid, s: S) -> Result { +pub fn hex_serialize(txid: &Txid, s: S) -> Result { let inst = txid.to_hex(); s.serialize_str(inst.as_str()) } -fn hex_deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result { +pub fn hex_deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result { let inst_str = String::deserialize(d)?; Txid::from_hex(&inst_str).map_err(serde::de::Error::custom) } diff --git a/testnet/stacks-node/src/event_dispatcher.rs b/testnet/stacks-node/src/event_dispatcher.rs index ba0b42ee17..f025b7410c 100644 --- a/testnet/stacks-node/src/event_dispatcher.rs +++ b/testnet/stacks-node/src/event_dispatcher.rs @@ -34,9 +34,11 @@ use stacks::vm::events::{FTEventType, NFTEventType, STXEventType}; use stacks::vm::types::{AssetIdentifier, QualifiedContractIdentifier, Value}; use super::config::{EventKeyType, EventObserverConfig}; +use stacks::chainstate::burn::operations::BlockstackOperationType; use stacks::chainstate::burn::ConsensusHash; use stacks::chainstate::stacks::db::unconfirmed::ProcessedUnconfirmedState; use stacks::chainstate::stacks::miner::TransactionEvent; +use stacks::chainstate::stacks::TransactionPayload; #[derive(Debug, Clone)] struct EventObserver { @@ -49,6 +51,7 @@ struct ReceiptPayloadInfo<'a> { raw_result: String, raw_tx: String, contract_interface_json: serde_json::Value, + burnchain_op_json: serde_json::Value, } const STATUS_RESP_TRUE: &str = "success"; @@ -192,15 +195,29 @@ impl EventObserver { } } (true, Value::Response(_)) => STATUS_RESP_POST_CONDITION, - _ => unreachable!(), // Transaction results should always be a Value::Response type + _ => { + 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 (txid, raw_tx) = match tx { - TransactionOrigin::Burn(txid) => (txid.to_string(), "00".to_string()), + 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), + ), TransactionOrigin::Stacks(ref tx) => { let txid = tx.txid().to_string(); let bytes = tx.serialize_to_vec(); - (txid, bytes_to_hex(&bytes)) + (txid, bytes_to_hex(&bytes), json!(null)) } }; @@ -220,6 +237,7 @@ impl EventObserver { raw_result, raw_tx, contract_interface_json, + burnchain_op_json, } } @@ -237,6 +255,7 @@ impl EventObserver { "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())), From 9118a7f5fdc9448b413a1e6617b4bd0add8c1982 Mon Sep 17 00:00:00 2001 From: Aaron Blankstein Date: Tue, 21 Feb 2023 13:27:28 -0600 Subject: [PATCH 2/2] docs: update event-dispatcher docs --- docs/event-dispatcher.md | 224 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 223 insertions(+), 1 deletion(-) diff --git a/docs/event-dispatcher.md b/docs/event-dispatcher.md index a6a7678f97..96a66df5a8 100644 --- a/docs/event-dispatcher.md +++ b/docs/event-dispatcher.md @@ -64,7 +64,8 @@ Example: "txid": "0x3e04ada5426332bfef446ba0a06d124aace4ade5c11840f541bf88e2e919faf6", "microblock_sequence": "None", "microblock_hash": "None", - "microblock_parent_hash": "None" + "microblock_parent_hash": "None", + "burnchain_op": null, }, { "contract_abi": null, @@ -76,6 +77,7 @@ Example: "microblock_sequence": "3", "microblock_hash": "0x9304fcbcc6daf5ac3f264522e0df50eddb5be85df6ee8a9fc2384c54274daaac", "microblock_parent_hash": "0x4893ab44636023efa08374033428e44eca490582bd39a6e61f3b6cf749b474bd" + "burnchain_op": null, } ], "matured_miner_rewards": [ @@ -105,6 +107,226 @@ Example: } ``` +#### Layer 1-triggered transactions + +Some subnet transactions are broadcasted via the layer-1 (just as some +Stacks transactions are broadcasted via Bitcoin). These transactions +use the `burnchain_op` field of the `transaction` object to convey information +from the layer-1 operation. The following block payload contains an example of this: + +```json + { + "anchored_cost": { + "read_count": 9, + "read_length": 3621, + "runtime": 4325000, + "write_count": 3, + "write_length": 2 + }, + "block_hash": "0x6951c0d3cf1ce9169685c897fdb7eee594fc232e805560117859d71b08f9c8e3", + "block_height": 6, + "burn_block_hash": "0x1c712cfaf83f8f9bc5990b611a18317e44497028e4d4440331dac0313802b01a", + "burn_block_height": 18, + "burn_block_time": 0, + "confirmed_microblocks_cost": { + "read_count": 0, + "read_length": 0, + "runtime": 0, + "write_count": 0, + "write_length": 0 + }, + "events": [ + { + "committed": true, + "event_index": 1, + "nft_mint_event": { + "asset_identifier": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8.simple-nft::nft-token", + "raw_value": "0x0100000000000000000000000000000001", + "recipient": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8", + "value": { + "UInt": 1 + } + }, + "txid": "0x7c558d682960d87f0af8f2904a5b561cdfb1732c878a048fc280454282e9eeb2", + "type": "nft_mint_event" + }, + { + "committed": true, + "event_index": 0, + "ft_mint_event": { + "amount": "1", + "asset_identifier": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8.simple-ft::ft-token", + "recipient": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8" + }, + "txid": "0x37eeac70800f4d6b6f18948d8987c26410a288afdbf6b0a30daeb17daaf300bc", + "type": "ft_mint_event" + } + ], + "index_block_hash": "0xdb379f4c3e43d3ee986ceff9e63c8fb222d855a640fe230ce33d080169315eee", + "matured_miner_rewards": [], + "miner_txid": "0x5af873ac3ded0e8041c539a66ecec3e85ec467a63fdda2526c6eeab6ad4fc416", + "parent_block_hash": "0x2bb521ae1d336e23a2e750d2dbbb2abfebc8f0295a9d4391cd72c40f0060a3fd", + "parent_burn_block_hash": "0x44e53851d23867abaf86abf0ad4013c9cfffbccb36afbb5c319b2d58743db4ce", + "parent_burn_block_height": 16, + "parent_burn_block_timestamp": 0, + "parent_index_block_hash": "0x4237bf67c1f20126ea67bc8b8beea1f80d44248e2d10290e6c2de97f6ed4a64d", + "parent_microblock": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parent_microblock_sequence": 0, + "transactions": [ + { + "burnchain_op": { + "deposit_ft": { + "amount": 1, + "burn_header_hash": "44e53851d23867abaf86abf0ad4013c9cfffbccb36afbb5c319b2d58743db4ce", + "l1_contract_id": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8.simple-ft", + "name": "ft-token", + "sender": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8", + "subnet_contract_id": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8.simple-ft", + "txid": "37eeac70800f4d6b6f18948d8987c26410a288afdbf6b0a30daeb17daaf300bc" + } + }, + "contract_abi": null, + "execution_cost": { + "read_count": 5, + "read_length": 1622, + "runtime": 1966000, + "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": "0x37eeac70800f4d6b6f18948d8987c26410a288afdbf6b0a30daeb17daaf300bc" + }, + { + "burnchain_op": { + "deposit_nft": { + "burn_header_hash": "44e53851d23867abaf86abf0ad4013c9cfffbccb36afbb5c319b2d58743db4ce", + "id": 1, + "l1_contract_id": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8.simple-nft", + "sender": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8", + "subnet_contract_id": "ST18F1AHKW194BWQ3CEFDPWVRARA79RBGFEWSDQR8.simple-nft", + "txid": "7c558d682960d87f0af8f2904a5b561cdfb1732c878a048fc280454282e9eeb2" + } + }, + "contract_abi": null, + "execution_cost": { + "read_count": 4, + "read_length": 1999, + "runtime": 2359000, + "write_count": 1, + "write_length": 1 + }, + "microblock_hash": null, + "microblock_parent_hash": null, + "microblock_sequence": null, + "raw_result": "0x0703", + "raw_tx": "0x00", + "status": "success", + "tx_index": 1, + "txid": "0x7c558d682960d87f0af8f2904a5b561cdfb1732c878a048fc280454282e9eeb2" + }, + { + "burnchain_op": null, + "contract_abi": null, + "execution_cost": { + "read_count": 0, + "read_length": 0, + "runtime": 0, + "write_count": 0, + "write_length": 0 + }, + "microblock_hash": null, + "microblock_parent_hash": null, + "microblock_sequence": null, + "raw_result": "0x0703", + "raw_tx": "0x800cf475620400a0e3473dd203d4f46ad5c24e5b444f5212e11d3b000000000000000500000000000000000001584c0b3805734fb438d2f2034c0503250effa85bf6a84e0a8d70122b86c5eb0b4fcf946276d515a5318576dc5b05234ab38d687046851e85deb72fb606da89cc010200000000040000000000000000000000000000000000000000000000000000000000000000", + "status": "success", + "tx_index": 2, + "txid": "0x427628fb9dc3a2848c551c4e1f6188138e030a091425574985f7af257757ee58" + } + ] + } +``` + +The `burnchain_op` field contains an "externally tagged" object. These example burnchain ops cover the whole of the +subnet burnchain_op enum: + +```json +{ + "deposit_stx": { + "amount": 7381273163198273, + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sender": "SP000000000000000000002Q6VF78", + "txid": "3333333333333333333333333333333333333333333333333333333333333333" + } +} +{ + "deposit_ft": { + "amount": 7381273163198273, + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "name": "ft-name", + "sender": "SP000000000000000000002Q6VF78.bns", + "subnet_contract_id": "SP000000000000000000002Q6VF78.bns", + "txid": "1111111111111111111111111111111111111111111111111111111111111111" + } +} +{ + "deposit_nft": { + "burn_header_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "id": 123123, + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "sender": "SP000000000000000000002Q6VF78.bns", + "subnet_contract_id": "SP000000000000000000002Q6VF78.bns", + "txid": "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + } +} +{ + "leader_block_commit": { + "block_header_hash": "1212121212121212121212121212121212121212121212121212121212121212", + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "txid": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1", + "withdrawal_merkle_root": "3131313131313131313131313131313131313131313131313131313131313131" + } +} +{ + "withdraw_stx": { + "amount": 7381273163198273, + "burn_header_hash": "babababababababababababababababababababababababababababababababa", + "recipient": "SP000000000000000000002Q6VF78", + "txid": "3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b3b" + } +} +{ + "withdraw_nft": { + "burn_header_hash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "id": 123123, + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "recipient": "SP000000000000000000002Q6VF78.bns", + "txid": "f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + } +} +{ + "withdraw_ft": { + "amount": 7381273163198273, + "burn_header_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "l1_contract_id": "SP000000000000000000002Q6VF78.bns", + "name": "ft-name", + "recipient": "SP000000000000000000002Q6VF78.bns", + "txid": "1111111111111111111111111111111111111111111111111111111111111111" + } +} +``` + +**Note** that withdraw operations and block commit operations on the +layer-1 do not impact the subnet's transaction state, so these +burnchain ops will not appear in transaction receipts. + ### `POST /new_burn_block` This payload includes information about burn blocks as their sortitions are processed.