Skip to content

Commit e4d2b1d

Browse files
committed
Merge #122: Add utility to validate GetMerkleRes
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
2 parents dacd772 + 54fd52d commit e4d2b1d

File tree

4 files changed

+85
-19
lines changed

4 files changed

+85
-19
lines changed

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ mod config;
6161
pub mod raw_client;
6262
mod stream;
6363
mod types;
64+
pub mod utils;
6465

6566
pub use api::ElectrumApi;
6667
pub use batch::Batch;

src/raw_client.rs

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,8 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
10791079
mod test {
10801080
use std::str::FromStr;
10811081

1082+
use crate::utils;
1083+
10821084
use super::RawClient;
10831085
use api::ElectrumApi;
10841086

@@ -1300,23 +1302,43 @@ mod test {
13001302

13011303
let client = RawClient::new(get_test_server(), None).unwrap();
13021304

1303-
let resp = client
1304-
.transaction_get_merkle(
1305-
&Txid::from_str("cc2ca076fd04c2aeed6d02151c447ced3d09be6fb4d4ef36cb5ed4e7a3260566")
1306-
.unwrap(),
1307-
630000,
1308-
)
1309-
.unwrap();
1305+
let txid =
1306+
Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d")
1307+
.unwrap();
1308+
let resp = client.transaction_get_merkle(&txid, 630000).unwrap();
13101309
assert_eq!(resp.block_height, 630000);
1311-
assert_eq!(resp.pos, 0);
1310+
assert_eq!(resp.pos, 68);
13121311
assert_eq!(resp.merkle.len(), 12);
13131312
assert_eq!(
13141313
resp.merkle[0],
13151314
[
1316-
30, 10, 161, 245, 132, 125, 136, 198, 186, 138, 107, 216, 92, 22, 145, 81, 130,
1317-
126, 200, 65, 121, 158, 105, 111, 38, 151, 38, 147, 144, 224, 5, 218
1315+
34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, 66,
1316+
179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25
13181317
]
13191318
);
1319+
1320+
// Check we can verify the merkle proof validity, but fail if we supply wrong data.
1321+
let block_header = client.block_header(resp.block_height).unwrap();
1322+
assert!(utils::validate_merkle_proof(
1323+
&txid,
1324+
&block_header.merkle_root,
1325+
&resp
1326+
));
1327+
1328+
let mut fail_resp = resp.clone();
1329+
fail_resp.pos = 13;
1330+
assert!(!utils::validate_merkle_proof(
1331+
&txid,
1332+
&block_header.merkle_root,
1333+
&fail_resp
1334+
));
1335+
1336+
let fail_block_header = client.block_header(resp.block_height + 1).unwrap();
1337+
assert!(!utils::validate_merkle_proof(
1338+
&txid,
1339+
&fail_block_header.merkle_root,
1340+
&resp
1341+
));
13201342
}
13211343

13221344
#[test]

src/types.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ where
161161
}
162162

163163
/// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request.
164-
#[derive(Debug, Deserialize)]
164+
#[derive(Clone, Debug, Deserialize)]
165165
pub struct GetHistoryRes {
166166
/// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of
167167
/// its inputs are unconfirmed too.
@@ -173,7 +173,7 @@ pub struct GetHistoryRes {
173173
}
174174

175175
/// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request.
176-
#[derive(Debug, Deserialize)]
176+
#[derive(Clone, Debug, Deserialize)]
177177
pub struct ListUnspentRes {
178178
/// Confirmation height of the transaction that created this output.
179179
pub height: usize,
@@ -186,7 +186,7 @@ pub struct ListUnspentRes {
186186
}
187187

188188
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
189-
#[derive(Debug, Deserialize)]
189+
#[derive(Clone, Debug, Deserialize)]
190190
pub struct ServerFeaturesRes {
191191
/// Server version reported.
192192
pub server_version: String,
@@ -204,7 +204,7 @@ pub struct ServerFeaturesRes {
204204
}
205205

206206
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
207-
#[derive(Debug, Deserialize)]
207+
#[derive(Clone, Debug, Deserialize)]
208208
pub struct GetHeadersRes {
209209
/// Maximum number of headers returned in a single response.
210210
pub max: usize,
@@ -219,7 +219,7 @@ pub struct GetHeadersRes {
219219
}
220220

221221
/// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request.
222-
#[derive(Debug, Deserialize)]
222+
#[derive(Clone, Debug, Deserialize)]
223223
pub struct GetBalanceRes {
224224
/// Confirmed balance in Satoshis for the address.
225225
pub confirmed: u64,
@@ -230,7 +230,7 @@ pub struct GetBalanceRes {
230230
}
231231

232232
/// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request.
233-
#[derive(Debug, Deserialize)]
233+
#[derive(Clone, Debug, Deserialize)]
234234
pub struct GetMerkleRes {
235235
/// Height of the block that confirmed the transaction
236236
pub block_height: usize,
@@ -242,7 +242,7 @@ pub struct GetMerkleRes {
242242
}
243243

244244
/// Notification of a new block header
245-
#[derive(Debug, Deserialize)]
245+
#[derive(Clone, Debug, Deserialize)]
246246
pub struct HeaderNotification {
247247
/// New block height.
248248
pub height: usize,
@@ -252,7 +252,7 @@ pub struct HeaderNotification {
252252
}
253253

254254
/// Notification of a new block header with the header encoded as raw bytes
255-
#[derive(Debug, Deserialize)]
255+
#[derive(Clone, Debug, Deserialize)]
256256
pub struct RawHeaderNotification {
257257
/// New block height.
258258
pub height: usize,
@@ -273,7 +273,7 @@ impl TryFrom<RawHeaderNotification> for HeaderNotification {
273273
}
274274

275275
/// Notification of the new status of a script
276-
#[derive(Debug, Deserialize)]
276+
#[derive(Clone, Debug, Deserialize)]
277277
pub struct ScriptNotification {
278278
/// Address that generated this notification.
279279
pub scripthash: ScriptHash,

src/utils.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//! Utilities helping to handle Electrum-related data.
2+
3+
use bitcoin::hash_types::TxMerkleNode;
4+
use bitcoin::hashes::sha256d::Hash as Sha256d;
5+
use bitcoin::hashes::Hash;
6+
use bitcoin::Txid;
7+
use types::GetMerkleRes;
8+
9+
/// Verifies a Merkle inclusion proof as retrieved via [`transaction_get_merkle`] for a transaction with the
10+
/// given `txid` and `merkle_root` as included in the [`BlockHeader`].
11+
///
12+
/// Returns `true` if the transaction is included in the corresponding block, and `false`
13+
/// otherwise.
14+
///
15+
/// [`transaction_get_merkle`]: crate::ElectrumApi::transaction_get_merkle
16+
/// [`BlockHeader`]: bitcoin::BlockHeader
17+
pub fn validate_merkle_proof(
18+
txid: &Txid,
19+
merkle_root: &TxMerkleNode,
20+
merkle_res: &GetMerkleRes,
21+
) -> bool {
22+
let mut index = merkle_res.pos;
23+
let mut cur = txid.to_raw_hash();
24+
for bytes in &merkle_res.merkle {
25+
let mut reversed = [0u8; 32];
26+
reversed.copy_from_slice(bytes);
27+
reversed.reverse();
28+
// unwrap() safety: `reversed` has len 32 so `from_slice` can never fail.
29+
let next_hash = Sha256d::from_slice(&reversed).unwrap();
30+
31+
let (left, right) = if index % 2 == 0 {
32+
(cur, next_hash)
33+
} else {
34+
(next_hash, cur)
35+
};
36+
37+
let data = [&left[..], &right[..]].concat();
38+
cur = Sha256d::hash(&data);
39+
index /= 2;
40+
}
41+
42+
cur == merkle_root.to_raw_hash()
43+
}

0 commit comments

Comments
 (0)