Skip to content

Commit

Permalink
Merge #122: Add utility to validate GetMerkleRes
Browse files Browse the repository at this point in the history
54fd52d Add test coverage for `validate_merkle_proof` (Elias Rohrer)
fe33e19 Add utility for validating a Merkle inclusion proof (Elias Rohrer)
dd872d6 Make response types `Clone` (Elias Rohrer)

Pull request description:

  I recently needed to validate a Merkle inclusion proof as retrieved via `transaction_get_merkle`.

  As I figured it might be useful to other people, too, we add it here as a simple utility method.

ACKs for top commit:
  notmandatory:
    ACK 54fd52d

Tree-SHA512: aac12160d5b91a011988f45013eb92924c2dfb244c1720e73dc5bcb731e69065c38e022502c756100d8ee6c9af06efa0de9bbfbb2b9e3c2e34d3223539206e1c
  • Loading branch information
notmandatory committed Dec 7, 2023
2 parents dacd772 + 54fd52d commit e4d2b1d
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ mod config;
pub mod raw_client;
mod stream;
mod types;
pub mod utils;

pub use api::ElectrumApi;
pub use batch::Batch;
Expand Down
42 changes: 32 additions & 10 deletions src/raw_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1079,6 +1079,8 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
mod test {
use std::str::FromStr;

use crate::utils;

use super::RawClient;
use api::ElectrumApi;

Expand Down Expand Up @@ -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]
Expand Down
18 changes: 9 additions & 9 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -273,7 +273,7 @@ impl TryFrom<RawHeaderNotification> 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,
Expand Down
43 changes: 43 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -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()
}

0 comments on commit e4d2b1d

Please sign in to comment.