Merge bitcoindevkit/rust-electrum-client#122: Add utility to validate GetMerkleRes

54fd52d898 Add test coverage for `validate_merkle_proof` (Elias Rohrer)
fe33e19bda Add utility for validating a Merkle inclusion proof (Elias Rohrer)
dd872d6714 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 54fd52d898

Tree-SHA512: aac12160d5b91a011988f45013eb92924c2dfb244c1720e73dc5bcb731e69065c38e022502c756100d8ee6c9af06efa0de9bbfbb2b9e3c2e34d3223539206e1c
This commit is contained in:
Steve Myers 2023-12-06 22:54:31 -06:00
commit e4d2b1d194
No known key found for this signature in database
GPG Key ID: 8105A46B22C2D051
4 changed files with 85 additions and 19 deletions

View File

@ -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;

View File

@ -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;
@ -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]

View File

@ -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<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,

43
src/utils.rs Normal file
View File

@ -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()
}