From daf55476c910a5b74ddb1a33c789f6d85587e86a Mon Sep 17 00:00:00 2001 From: Ludo Galabru Date: Wed, 12 Apr 2023 21:51:39 -0400 Subject: [PATCH] feat: ability to control inclusion of inputs/outputs/proofs/witness --- README.md | 12 ++++ components/chainhook-cli/src/cli/mod.rs | 4 ++ components/chainhook-cli/src/scan/bitcoin.rs | 9 ++- components/chainhook-cli/src/scan/stacks.rs | 4 +- components/chainhook-cli/src/service/mod.rs | 9 +-- .../src/chainhooks/bitcoin/mod.rs | 55 ++++++++++++------ .../src/chainhooks/types.rs | 50 ++++++---------- .../src/observer/mod.rs | 57 ++++++++++--------- .../src/observer/tests/mod.rs | 4 ++ 9 files changed, 113 insertions(+), 91 deletions(-) diff --git a/README.md b/README.md index b3f3866d..65ca8865 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,18 @@ Additional configuration knobs available: // Stop evaluating chainhook after a given number of occurrences found: "expire_after_occurrence": 1 +// Include proof: +"include_proof": false + +// Include Bitcoin transaction inputs in payload: +"include_inputs": false + +// Include Bitcoin transaction outputs in payload: +"include_outputs": false + +// Include Bitcoin transaction witness in payload: +"include_witness": false + ``` Putting all the pieces together: diff --git a/components/chainhook-cli/src/cli/mod.rs b/components/chainhook-cli/src/cli/mod.rs index 7f55dbda..9b9e62ae 100644 --- a/components/chainhook-cli/src/cli/mod.rs +++ b/components/chainhook-cli/src/cli/mod.rs @@ -470,6 +470,10 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> { action: HookAction::FileAppend(FileHook { path: "ordinals.txt".into(), }), + include_inputs: None, + include_outputs: None, + include_proof: None, + include_witness: None, }, ); diff --git a/components/chainhook-cli/src/scan/bitcoin.rs b/components/chainhook-cli/src/scan/bitcoin.rs index ce9fb895..f64b9b42 100644 --- a/components/chainhook-cli/src/scan/bitcoin.rs +++ b/components/chainhook-cli/src/scan/bitcoin.rs @@ -272,9 +272,12 @@ pub async fn execute_predicates_action<'a>( ctx: &Context, ) -> Result { let mut actions_triggered = 0; - let proofs = gather_proofs(&hits, &config, &ctx); - for hit in hits.into_iter() { - match handle_bitcoin_hook_action(hit, &proofs) { + let mut proofs = HashMap::new(); + for trigger in hits.into_iter() { + if trigger.chainhook.include_proof { + gather_proofs(&trigger, &mut proofs, &config, &ctx); + } + match handle_bitcoin_hook_action(trigger, &proofs) { Err(e) => { error!(ctx.expect_logger(), "unable to handle action {}", e); } diff --git a/components/chainhook-cli/src/scan/stacks.rs b/components/chainhook-cli/src/scan/stacks.rs index 1fd71736..d4a8c95f 100644 --- a/components/chainhook-cli/src/scan/stacks.rs +++ b/components/chainhook-cli/src/scan/stacks.rs @@ -9,9 +9,7 @@ use crate::{ config::Config, }; use chainhook_event_observer::{ - chainhooks::{ - stacks::evaluate_stacks_chainhook_on_blocks, - }, + chainhooks::stacks::evaluate_stacks_chainhook_on_blocks, indexer::{self, stacks::standardize_stacks_serialized_block_header, Indexer}, utils::Context, }; diff --git a/components/chainhook-cli/src/service/mod.rs b/components/chainhook-cli/src/service/mod.rs index 921f7ad5..ca109ce5 100644 --- a/components/chainhook-cli/src/service/mod.rs +++ b/components/chainhook-cli/src/service/mod.rs @@ -4,17 +4,12 @@ use crate::scan::stacks::scan_stacks_chainstate_via_csv_using_predicate; use chainhook_event_observer::chainhooks::types::{ChainhookConfig, ChainhookFullSpecification}; +use chainhook_event_observer::chainhooks::types::ChainhookSpecification; use chainhook_event_observer::observer::{start_event_observer, ApiKey, ObserverEvent}; use chainhook_event_observer::utils::Context; -use chainhook_event_observer::{ - chainhooks::types::ChainhookSpecification, -}; -use chainhook_types::{ - BitcoinBlockSignaling, StacksBlockData, StacksChainEvent, -}; +use chainhook_types::{BitcoinBlockSignaling, StacksBlockData, StacksChainEvent}; use redis::{Commands, Connection}; - use std::sync::mpsc::channel; pub const DEFAULT_INGESTION_PORT: u16 = 20455; diff --git a/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs b/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs index 50a2f49e..9a429242 100644 --- a/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs +++ b/components/chainhook-event-observer/src/chainhooks/bitcoin/mod.rs @@ -130,14 +130,14 @@ pub fn serialize_bitcoin_payload_to_json<'a>( trigger: BitcoinTriggerChainhook<'a>, proofs: &HashMap<&'a TransactionIdentifier, String>, ) -> JsonValue { - let predicate = &trigger.chainhook.predicate; + let predicate_spec = &trigger.chainhook; json!({ "apply": trigger.apply.into_iter().map(|(transactions, block)| { json!({ "block_identifier": block.block_identifier, "parent_block_identifier": block.parent_block_identifier, "timestamp": block.timestamp, - "transactions": serialize_bitcoin_transactions_to_json(&predicate, &transactions, proofs), + "transactions": serialize_bitcoin_transactions_to_json(&predicate_spec, &transactions, proofs), "metadata": block.metadata, }) }).collect::>(), @@ -146,7 +146,7 @@ pub fn serialize_bitcoin_payload_to_json<'a>( "block_identifier": block.block_identifier, "parent_block_identifier": block.parent_block_identifier, "timestamp": block.timestamp, - "transactions": serialize_bitcoin_transactions_to_json(&predicate, &transactions, proofs), + "transactions": serialize_bitcoin_transactions_to_json(&predicate_spec, &transactions, proofs), "metadata": block.metadata, }) }).collect::>(), @@ -158,36 +158,57 @@ pub fn serialize_bitcoin_payload_to_json<'a>( } pub fn serialize_bitcoin_transactions_to_json<'a>( - predicate: &BitcoinPredicateType, + predicate_spec: &BitcoinChainhookSpecification, transactions: &Vec<&BitcoinTransactionData>, proofs: &HashMap<&'a TransactionIdentifier, String>, ) -> Vec { - transactions.into_iter().map(|transaction| { + transactions + .into_iter() + .map(|transaction| { let mut metadata = serde_json::Map::new(); - if predicate.include_inputs() { - metadata.insert("inputs".into(), json!(transaction.metadata.inputs.iter().map(|input| { - json!({ - "txin": format!("0x{}:{}", input.previous_output.txid, input.previous_output.vout), - "sequence": input.sequence, - }) - }).collect::>())); + if predicate_spec.include_inputs { + metadata.insert( + "inputs".into(), + json!(transaction + .metadata + .inputs + .iter() + .map(|input| { + json!({ + "txin": format!("0x{}", input.previous_output.txid), + "vout": input.previous_output.vout, + "sequence": input.sequence, + }) + }) + .collect::>()), + ); } - if predicate.include_outputs() { + 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)); + 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)); + metadata.insert( + "ordinal_operations".into(), + json!(transaction.metadata.ordinal_operations), + ); } - metadata.insert("proof".into(), json!(proofs.get(&transaction.transaction_identifier))); + metadata.insert( + "proof".into(), + json!(proofs.get(&transaction.transaction_identifier)), + ); json!({ "transaction_identifier": transaction.transaction_identifier, "operations": transaction.operations, "metadata": metadata }) - }).collect::>() + }) + .collect::>() } pub fn handle_bitcoin_hook_action<'a>( diff --git a/components/chainhook-event-observer/src/chainhooks/types.rs b/components/chainhook-event-observer/src/chainhooks/types.rs index 294fa761..8f78974e 100644 --- a/components/chainhook-event-observer/src/chainhooks/types.rs +++ b/components/chainhook-event-observer/src/chainhooks/types.rs @@ -217,6 +217,10 @@ pub struct BitcoinChainhookSpecification { pub expire_after_occurrence: Option, pub predicate: BitcoinPredicateType, pub action: HookAction, + pub include_proof: bool, + pub include_inputs: bool, + pub include_outputs: bool, + pub include_witness: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] @@ -283,6 +287,10 @@ impl BitcoinChainhookFullSpecification { expire_after_occurrence: spec.expire_after_occurrence, predicate: spec.predicate, action: spec.action, + include_proof: spec.include_proof.unwrap_or(false), + include_inputs: spec.include_inputs.unwrap_or(false), + include_outputs: spec.include_outputs.unwrap_or(false), + include_witness: spec.include_witness.unwrap_or(false), }) } } @@ -295,6 +303,14 @@ pub struct BitcoinChainhookNetworkSpecification { pub end_block: Option, #[serde(skip_serializing_if = "Option::is_none")] pub expire_after_occurrence: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_proof: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_inputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_outputs: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub include_witness: Option, #[serde(rename = "if_this")] pub predicate: BitcoinPredicateType, #[serde(rename = "then_that")] @@ -456,40 +472,6 @@ pub enum BitcoinPredicateType { Protocol(Protocols), } -impl BitcoinPredicateType { - pub fn include_inputs(&self) -> bool { - match &self { - BitcoinPredicateType::Block => true, - BitcoinPredicateType::Txid(_rules) => true, - BitcoinPredicateType::Inputs(_rules) => true, - BitcoinPredicateType::Outputs(_rules) => false, - BitcoinPredicateType::Protocol(Protocols::Ordinal(_)) => false, - BitcoinPredicateType::Protocol(Protocols::Stacks(_)) => false, - } - } - - pub fn include_outputs(&self) -> bool { - match &self { - BitcoinPredicateType::Block => true, - BitcoinPredicateType::Txid(_rules) => true, - BitcoinPredicateType::Inputs(_rules) => false, - BitcoinPredicateType::Outputs(_rules) => true, - BitcoinPredicateType::Protocol(Protocols::Ordinal(_)) => true, - BitcoinPredicateType::Protocol(Protocols::Stacks(_)) => false, - } - } - - pub fn include_witness(&self) -> bool { - match &self { - BitcoinPredicateType::Block => true, - BitcoinPredicateType::Txid(_rules) => true, - BitcoinPredicateType::Inputs(_rules) => false, - BitcoinPredicateType::Outputs(_rules) => false, - BitcoinPredicateType::Protocol(_rules) => false, - } - } -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum InputPredicate { diff --git a/components/chainhook-event-observer/src/observer/mod.rs b/components/chainhook-event-observer/src/observer/mod.rs index 6f27d02b..c615652b 100644 --- a/components/chainhook-event-observer/src/observer/mod.rs +++ b/components/chainhook-event-observer/src/observer/mod.rs @@ -536,10 +536,11 @@ pub fn apply_bitcoin_block() {} pub fn rollback_bitcoin_block() {} pub fn gather_proofs<'a>( - chainhooks_to_trigger: &Vec>, + trigger: &BitcoinTriggerChainhook<'a>, + proofs: &mut HashMap<&'a TransactionIdentifier, String>, config: &EventObserverConfig, ctx: &Context, -) -> HashMap<&'a TransactionIdentifier, String> { +) { let bitcoin_client_rpc = Client::new( &config.bitcoind_rpc_url, Auth::UserPass( @@ -549,35 +550,31 @@ pub fn gather_proofs<'a>( ) .expect("unable to build http client"); - let mut proofs = HashMap::new(); - for hook_to_trigger in chainhooks_to_trigger.iter() { - for (transactions, block) in hook_to_trigger.apply.iter() { - for transaction in transactions.iter() { - if !proofs.contains_key(&transaction.transaction_identifier) { - ctx.try_log(|logger| { - slog::info!( - logger, - "Collecting proof for transaction {}", - transaction.transaction_identifier.hash - ) - }); - match get_bitcoin_proof( - &bitcoin_client_rpc, - &transaction.transaction_identifier, - &block.block_identifier, - ) { - Ok(proof) => { - proofs.insert(&transaction.transaction_identifier, proof); - } - Err(e) => { - ctx.try_log(|logger| slog::error!(logger, "{e}")); - } + for (transactions, block) in trigger.apply.iter() { + for transaction in transactions.iter() { + if !proofs.contains_key(&transaction.transaction_identifier) { + ctx.try_log(|logger| { + slog::info!( + logger, + "Collecting proof for transaction {}", + transaction.transaction_identifier.hash + ) + }); + match get_bitcoin_proof( + &bitcoin_client_rpc, + &transaction.transaction_identifier, + &block.block_identifier, + ) { + Ok(proof) => { + proofs.insert(&transaction.transaction_identifier, proof); + } + Err(e) => { + ctx.try_log(|logger| slog::error!(logger, "{e}")); } } } } } - proofs } pub async fn start_observer_commands_handler( @@ -939,7 +936,13 @@ pub async fn start_observer_commands_handler( } } - let proofs = gather_proofs(&chainhooks_to_trigger, &config, &ctx); + let mut proofs = HashMap::new(); + for trigger in chainhooks_to_trigger.iter() { + if trigger.chainhook.include_proof { + gather_proofs(&trigger, &mut proofs, &config, &ctx); + } + } + ctx.try_log(|logger| { slog::info!( logger, diff --git a/components/chainhook-event-observer/src/observer/tests/mod.rs b/components/chainhook-event-observer/src/observer/tests/mod.rs index 853a6c2c..f4702466 100644 --- a/components/chainhook-event-observer/src/observer/tests/mod.rs +++ b/components/chainhook-event-observer/src/observer/tests/mod.rs @@ -100,6 +100,10 @@ fn bitcoin_chainhook_p2pkh( ExactMatchingRule::Equals(address.to_string()), )), action: HookAction::Noop, + include_proof: None, + include_inputs: None, + include_outputs: None, + include_witness: None, }, );