From 5f20a869acbd057d855b9601a4fb1072e75ab4c4 Mon Sep 17 00:00:00 2001 From: Micaiah Reid Date: Thu, 25 Apr 2024 11:02:12 -0400 Subject: [PATCH] fix: make bitcoin payload serialization deserializable (#569) When a Chainhook is triggered and we send a payload to a user, we filter some values based on user settings (`include_proof`, `include_inputs`, etc). This helps to reduce the payload size that is sent over the wire. However, when we created this payload, we were omitting these fields completely even though the corresponding type does not have those fields as optional. This makes using our SDK types for deserialization impossible. This PR adds in those fields in all cases and sets them to empty values if filtered out. --- .../src/chainhooks/bitcoin/mod.rs | 81 ++++++++++++------- .../src/chainhooks/bitcoin/tests.rs | 59 ++++++++++++++ components/chainhook-sdk/src/observer/mod.rs | 6 +- 3 files changed, 113 insertions(+), 33 deletions(-) diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs index 08de8dfc6..cc1900b63 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs @@ -35,6 +35,7 @@ pub struct BitcoinTriggerChainhook<'a> { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct BitcoinTransactionPayload { + #[serde(flatten)] pub block: BitcoinBlockData, } @@ -233,38 +234,60 @@ pub fn serialize_bitcoin_transactions_to_json<'a>( .into_iter() .map(|transaction| { let mut metadata = serde_json::Map::new(); - if predicate_spec.include_inputs { - metadata.insert( - "inputs".into(), - json!(transaction - .metadata - .inputs - .iter() - .map(|input| { - json!({ + + metadata.insert("fee".into(), json!(transaction.metadata.fee)); + metadata.insert("index".into(), json!(transaction.metadata.index)); + + let inputs = if predicate_spec.include_inputs { + transaction + .metadata + .inputs + .iter() + .map(|input| { + let witness = if predicate_spec.include_witness { + input.witness.clone() + } else { + vec![] + }; + json!({ + "previous_output": { "txin": input.previous_output.txid.hash.to_string(), "vout": input.previous_output.vout, - "sequence": input.sequence, - }) + "value": input.previous_output.value, + "block_height": input.previous_output.block_height, + }, + "script_sig": input.script_sig, + "sequence": input.sequence, + "witness": witness }) - .collect::>()), - ); - } - if predicate_spec.include_outputs { - metadata.insert("outputs".into(), json!(transaction.metadata.outputs)); - } - if !transaction.metadata.stacks_operations.is_empty() { - metadata.insert( - "stacks_operations".into(), - json!(transaction.metadata.stacks_operations), - ); - } - if !transaction.metadata.ordinal_operations.is_empty() { - metadata.insert( - "ordinal_operations".into(), - json!(transaction.metadata.ordinal_operations), - ); - } + }) + .collect::>() + } else { + vec![] + }; + metadata.insert("inputs".into(), json!(inputs)); + + let outputs = if predicate_spec.include_outputs { + transaction.metadata.outputs.clone() + } else { + vec![] + }; + metadata.insert("outputs".into(), json!(outputs)); + + let stacks_ops = if transaction.metadata.stacks_operations.is_empty() { + vec![] + } else { + transaction.metadata.stacks_operations.clone() + }; + metadata.insert("stacks_operations".into(), json!(stacks_ops)); + + let ordinals_ops = if transaction.metadata.ordinal_operations.is_empty() { + vec![] + } else { + transaction.metadata.ordinal_operations.clone() + }; + metadata.insert("ordinal_operations".into(), json!(ordinals_ops)); + metadata.insert( "proof".into(), json!(proofs.get(&transaction.transaction_identifier)), diff --git a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs index 032a2cd84..2f879f1ff 100644 --- a/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs +++ b/components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs @@ -1,8 +1,12 @@ use super::super::types::MatchingRule; use super::*; +use crate::indexer::tests::helpers::accounts; +use crate::indexer::tests::helpers::bitcoin_blocks::generate_test_bitcoin_block; +use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer; use crate::types::BitcoinTransactionMetadata; use chainhook_types::bitcoin::TxOut; +use chainhook_types::BitcoinNetwork; use test_case::test_case; #[test_case( @@ -134,3 +138,58 @@ fn script_pubkey_evaluation(output: OutputPredicate, script_pubkey: &str, matche assert_eq!(matches, predicate.evaluate_transaction_predicate(&tx, &ctx)); } + +#[test_case( + true, true, true, true; + "including all optional fields" +)] +#[test_case( + false, false, false, false; + "omitting all optional fields" +)] + +fn it_serdes_occurrence_payload( + include_proof: bool, + include_inputs: bool, + include_outputs: bool, + include_witness: bool, +) { + let transaction = generate_test_tx_bitcoin_p2pkh_transfer( + 0, + &accounts::wallet_1_btc_address(), + &accounts::wallet_3_btc_address(), + 3, + ); + let block = generate_test_bitcoin_block(0, 0, vec![transaction.clone()], None); + let chainhook = &BitcoinChainhookSpecification { + uuid: "uuid".into(), + owner_uuid: None, + name: "name".into(), + network: BitcoinNetwork::Mainnet, + version: 0, + blocks: None, + start_block: None, + end_block: None, + expire_after_occurrence: None, + predicate: BitcoinPredicateType::Block, + action: HookAction::Noop, + include_proof, + include_inputs, + include_outputs, + include_witness, + enabled: true, + expired_at: None, + }; + let trigger = BitcoinTriggerChainhook { + chainhook, + apply: vec![(vec![&transaction], &block)], + rollback: vec![], + }; + let payload = serde_json::to_vec(&serialize_bitcoin_payload_to_json( + &trigger, + &HashMap::new(), + )) + .unwrap(); + + let _: BitcoinChainhookOccurrencePayload = serde_json::from_slice(&payload[..]).unwrap(); +} diff --git a/components/chainhook-sdk/src/observer/mod.rs b/components/chainhook-sdk/src/observer/mod.rs index a805d2314..e5b7a7def 100644 --- a/components/chainhook-sdk/src/observer/mod.rs +++ b/components/chainhook-sdk/src/observer/mod.rs @@ -522,21 +522,19 @@ pub fn start_event_observer( pub async fn start_bitcoin_event_observer( config: EventObserverConfig, - _observer_commands_tx: Sender, + observer_commands_tx: Sender, observer_commands_rx: Receiver, observer_events_tx: Option>, observer_sidecar: Option, ctx: Context, ) -> Result<(), Box> { let chainhook_store = config.get_chainhook_store(); - #[cfg(feature = "zeromq")] { let ctx_moved = ctx.clone(); let config_moved = config.clone(); let _ = hiro_system_kit::thread_named("ZMQ handler").spawn(move || { - let future = - zmq::start_zeromq_runloop(&config_moved, _observer_commands_tx, &ctx_moved); + let future = zmq::start_zeromq_runloop(&config_moved, observer_commands_tx, &ctx_moved); let _ = hiro_system_kit::nestable_block_on(future); }); }