From dd872d67147a2abcae63f4450770f0a0d95c45cb Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 27 Oct 2023 16:32:51 +0200 Subject: [PATCH 1/3] Make response types `Clone` .. as it's convenient. --- src/types.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/types.rs b/src/types.rs index 0773883..ecbc57a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -161,7 +161,7 @@ where } /// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct GetHistoryRes { /// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of /// its inputs are unconfirmed too. @@ -173,7 +173,7 @@ pub struct GetHistoryRes { } /// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ListUnspentRes { /// Confirmation height of the transaction that created this output. pub height: usize, @@ -186,7 +186,7 @@ pub struct ListUnspentRes { } /// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ServerFeaturesRes { /// Server version reported. pub server_version: String, @@ -204,7 +204,7 @@ pub struct ServerFeaturesRes { } /// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct GetHeadersRes { /// Maximum number of headers returned in a single response. pub max: usize, @@ -219,7 +219,7 @@ pub struct GetHeadersRes { } /// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct GetBalanceRes { /// Confirmed balance in Satoshis for the address. pub confirmed: u64, @@ -230,7 +230,7 @@ pub struct GetBalanceRes { } /// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct GetMerkleRes { /// Height of the block that confirmed the transaction pub block_height: usize, @@ -242,7 +242,7 @@ pub struct GetMerkleRes { } /// Notification of a new block header -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct HeaderNotification { /// New block height. pub height: usize, @@ -252,7 +252,7 @@ pub struct HeaderNotification { } /// Notification of a new block header with the header encoded as raw bytes -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct RawHeaderNotification { /// New block height. pub height: usize, @@ -273,7 +273,7 @@ impl TryFrom for HeaderNotification { } /// Notification of the new status of a script -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ScriptNotification { /// Address that generated this notification. pub scripthash: ScriptHash, From fe33e19bdabb5da627731c5ca94fac42e7a53882 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 27 Oct 2023 16:33:40 +0200 Subject: [PATCH 2/3] Add utility for validating a Merkle inclusion proof ... as retrieved via `transaction_get_merkle`. --- src/lib.rs | 1 + src/utils.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 src/utils.rs diff --git a/src/lib.rs b/src/lib.rs index 269714f..07828ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,6 +63,7 @@ mod config; pub mod raw_client; mod stream; mod types; +pub mod utils; pub use api::ElectrumApi; pub use batch::Batch; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..c53900f --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,43 @@ +//! Utilities helping to handle Electrum-related data. + +use bitcoin::hash_types::TxMerkleNode; +use bitcoin::hashes::sha256d::Hash as Sha256d; +use bitcoin::hashes::Hash; +use bitcoin::Txid; +use types::GetMerkleRes; + +/// Verifies a Merkle inclusion proof as retrieved via [`transaction_get_merkle`] for a transaction with the +/// given `txid` and `merkle_root` as included in the [`BlockHeader`]. +/// +/// Returns `true` if the transaction is included in the corresponding block, and `false` +/// otherwise. +/// +/// [`transaction_get_merkle`]: crate::ElectrumApi::transaction_get_merkle +/// [`BlockHeader`]: bitcoin::BlockHeader +pub fn validate_merkle_proof( + txid: &Txid, + merkle_root: &TxMerkleNode, + merkle_res: &GetMerkleRes, +) -> bool { + let mut index = merkle_res.pos; + let mut cur = txid.to_raw_hash(); + for bytes in &merkle_res.merkle { + let mut reversed = [0u8; 32]; + reversed.copy_from_slice(bytes); + reversed.reverse(); + // unwrap() safety: `reversed` has len 32 so `from_slice` can never fail. + let next_hash = Sha256d::from_slice(&reversed).unwrap(); + + let (left, right) = if index % 2 == 0 { + (cur, next_hash) + } else { + (next_hash, cur) + }; + + let data = [&left[..], &right[..]].concat(); + cur = Sha256d::hash(&data); + index /= 2; + } + + cur == merkle_root.to_raw_hash() +} From 54fd52d898bd8cfd448a4d303f12c156a7a33201 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 27 Oct 2023 16:34:34 +0200 Subject: [PATCH 3/3] Add test coverage for `validate_merkle_proof` --- src/raw_client.rs | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/raw_client.rs b/src/raw_client.rs index 550b8aa..d8ade2e 100644 --- a/src/raw_client.rs +++ b/src/raw_client.rs @@ -1079,6 +1079,8 @@ impl ElectrumApi for RawClient { mod test { use std::str::FromStr; + use crate::utils; + use super::RawClient; use api::ElectrumApi; @@ -1300,23 +1302,43 @@ mod test { let client = RawClient::new(get_test_server(), None).unwrap(); - let resp = client - .transaction_get_merkle( - &Txid::from_str("cc2ca076fd04c2aeed6d02151c447ced3d09be6fb4d4ef36cb5ed4e7a3260566") - .unwrap(), - 630000, - ) - .unwrap(); + let txid = + Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d") + .unwrap(); + let resp = client.transaction_get_merkle(&txid, 630000).unwrap(); assert_eq!(resp.block_height, 630000); - assert_eq!(resp.pos, 0); + assert_eq!(resp.pos, 68); assert_eq!(resp.merkle.len(), 12); assert_eq!( resp.merkle[0], [ - 30, 10, 161, 245, 132, 125, 136, 198, 186, 138, 107, 216, 92, 22, 145, 81, 130, - 126, 200, 65, 121, 158, 105, 111, 38, 151, 38, 147, 144, 224, 5, 218 + 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 ] ); + + // Check we can verify the merkle proof validity, but fail if we supply wrong data. + let block_header = client.block_header(resp.block_height).unwrap(); + assert!(utils::validate_merkle_proof( + &txid, + &block_header.merkle_root, + &resp + )); + + let mut fail_resp = resp.clone(); + fail_resp.pos = 13; + assert!(!utils::validate_merkle_proof( + &txid, + &block_header.merkle_root, + &fail_resp + )); + + let fail_block_header = client.block_header(resp.block_height + 1).unwrap(); + assert!(!utils::validate_merkle_proof( + &txid, + &fail_block_header.merkle_root, + &resp + )); } #[test]