From 2dfc299b7ab175e4a62f0797b8f69070415d2afc Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Wed, 2 Jul 2025 23:54:11 -0300 Subject: [PATCH] feat: add `batch_transaction_get_merkle` - adds the new batch method for `blockchain.transaction.get_merkle`. - adds a new test for `batch_transaction_get_merkle` with 3 different txids and block_heights. --- src/api.rs | 33 +++++++++++++++ src/batch.rs | 11 +++++ src/client.rs | 16 +++++++ src/raw_client.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+) diff --git a/src/api.rs b/src/api.rs index 16f32b4..86cbc32 100644 --- a/src/api.rs +++ b/src/api.rs @@ -145,6 +145,17 @@ where (**self).transaction_get_merkle(txid, height) } + fn batch_transaction_get_merkle<'s, I>( + &self, + txids_and_heights: I, + ) -> Result, Error> + where + I: IntoIterator + Clone, + I::Item: Borrow<&'s (Txid, usize)>, + { + (**self).batch_transaction_get_merkle(txids_and_heights) + } + fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { (**self).txid_from_pos(height, tx_pos) } @@ -362,6 +373,17 @@ pub trait ElectrumApi { /// Returns the merkle path for the transaction `txid` confirmed in the block at `height`. fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result; + /// Batch version of [`transaction_get_merkle`](#method.transaction_get_merkle). + /// + /// Take a list of `(txid, height)`, for transactions with `txid` confirmed in the block at `height`. + fn batch_transaction_get_merkle<'s, I>( + &self, + txids_and_heights: I, + ) -> Result, Error> + where + I: IntoIterator + Clone, + I::Item: Borrow<&'s (Txid, usize)>; + /// Returns a transaction hash, given a block `height` and a `tx_pos` in the block. fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result; @@ -558,6 +580,17 @@ mod test { unreachable!() } + fn batch_transaction_get_merkle<'s, I>( + &self, + _: I, + ) -> Result, crate::Error> + where + I: IntoIterator + Clone, + I::Item: std::borrow::Borrow<&'s (bitcoin::Txid, usize)>, + { + unreachable!() + } + fn txid_from_pos(&self, _: usize, _: usize) -> Result { unreachable!() } diff --git a/src/batch.rs b/src/batch.rs index 88b17c3..b13d72f 100644 --- a/src/batch.rs +++ b/src/batch.rs @@ -62,6 +62,17 @@ impl Batch { .push((String::from("blockchain.transaction.get"), params)); } + /// Add one `blockchain.transaction.get_merkle` request to the batch queue + pub fn transaction_get_merkle(&mut self, tx_hash_and_height: &(Txid, usize)) { + let (tx_hash, height) = tx_hash_and_height; + let params = vec![ + Param::String(format!("{:x}", tx_hash)), + Param::Usize(*height), + ]; + self.calls + .push((String::from("blockchain.transaction.get_merkle"), params)); + } + /// Add one `blockchain.estimatefee` request to the batch queue pub fn estimate_fee(&mut self, number: usize) { let params = vec![Param::Usize(number)]; diff --git a/src/client.rs b/src/client.rs index 5cdf0d8..1da6925 100644 --- a/src/client.rs +++ b/src/client.rs @@ -327,6 +327,22 @@ impl ElectrumApi for Client { impl_inner_call!(self, transaction_get_merkle, txid, height) } + #[inline] + fn batch_transaction_get_merkle<'s, I>( + &self, + txids_and_heights: I, + ) -> Result, Error> + where + I: IntoIterator + Clone, + I::Item: Borrow<&'s (Txid, usize)>, + { + impl_inner_call!( + self, + batch_transaction_get_merkle, + txids_and_heights.clone() + ) + } + #[inline] fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { impl_inner_call!(self, txid_from_pos, height, tx_pos) diff --git a/src/raw_client.rs b/src/raw_client.rs index 45af548..7a5460c 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -1102,6 +1102,17 @@ impl ElectrumApi for RawClient { Ok(serde_json::from_value(result)?) } + fn batch_transaction_get_merkle<'s, I>( + &self, + txids_and_hashes: I, + ) -> Result, Error> + where + I: IntoIterator + Clone, + I::Item: Borrow<&'s (Txid, usize)>, + { + impl_batch_call!(self, txids_and_hashes, transaction_get_merkle) + } + fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { let params = vec![Param::Usize(height), Param::Usize(tx_pos)]; let req = Request::new_id( @@ -1448,6 +1459,98 @@ mod test { )); } + #[test] + fn test_batch_transaction_get_merkle() { + use bitcoin::Txid; + + struct TestCase { + txid: Txid, + block_height: usize, + exp_pos: usize, + exp_bytes: [u8; 32], + } + + let client = RawClient::new(get_test_server(), None).unwrap(); + + let test_cases: Vec = vec![ + TestCase { + txid: Txid::from_str( + "1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d", + ) + .unwrap(), + block_height: 630000, + exp_pos: 68, + exp_bytes: [ + 34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, + 66, 179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25, + ], + }, + TestCase { + txid: Txid::from_str( + "70a8639bc9b743c0610d1231103a2f8e99f4a25670946b91f16c55a5373b37d1", + ) + .unwrap(), + block_height: 630001, + exp_pos: 25, + exp_bytes: [ + 169, 100, 34, 99, 168, 101, 25, 168, 184, 90, 77, 50, 151, 245, 130, 101, 193, + 229, 136, 128, 63, 110, 241, 19, 242, 59, 184, 137, 245, 249, 188, 110, + ], + }, + TestCase { + txid: Txid::from_str( + "a0db149ace545beabbd87a8d6b20ffd6aa3b5a50e58add49a3d435f898c272cf", + ) + .unwrap(), + block_height: 840000, + exp_pos: 0, + exp_bytes: [ + 43, 184, 95, 75, 0, 75, 230, 218, 84, 247, 102, 193, 124, 30, 133, 81, 135, 50, + 113, 18, 194, 49, 239, 47, 243, 94, 186, 208, 234, 103, 198, 158, + ], + }, + ]; + + let txids_and_heights: Vec<(Txid, usize)> = test_cases + .iter() + .map(|case| (case.txid, case.block_height)) + .collect(); + + let resp = client + .batch_transaction_get_merkle(&txids_and_heights) + .unwrap(); + + for (i, (res, test_case)) in resp.iter().zip(test_cases).enumerate() { + assert_eq!(res.block_height, test_case.block_height); + assert_eq!(res.pos, test_case.exp_pos); + assert_eq!(res.merkle.len(), 12); + assert_eq!(res.merkle[0], test_case.exp_bytes); + + // Check we can verify the merkle proof validity, but fail if we supply wrong data. + let block_header = client.block_header(res.block_height).unwrap(); + assert!(utils::validate_merkle_proof( + &txids_and_heights[i].0, + &block_header.merkle_root, + res + )); + + let mut fail_res = res.clone(); + fail_res.pos = 13; + assert!(!utils::validate_merkle_proof( + &txids_and_heights[i].0, + &block_header.merkle_root, + &fail_res + )); + + let fail_block_header = client.block_header(res.block_height + 1).unwrap(); + assert!(!utils::validate_merkle_proof( + &txids_and_heights[i].0, + &fail_block_header.merkle_root, + res + )); + } + } + #[test] fn test_txid_from_pos() { use bitcoin::Txid;