Skip to content

Commit ad21685

Browse files
committed
Merge bitcoindevkit/bdk#1837: bdk_electrum: Handle negative heights properly
161d715 test(electrum): Chained mempool sync (志宇) 7502052 fix(electrum): Handle negative heights properly (志宇) Pull request description: ### Description This fixes a problem with `bdk_electrum` making bogus `transaction_get_merkle` calls to the Electrum server. In electrum, heights returned alongside txs may be 0 or -1. 0 means the tx is unconfirmed. -1 means the tx is unconfirmed and spends an unconfirmed tx. Previously, the codebase assumed that heights cannot be negative and used a `i32 as usize` cast (which would lead to the casted height being 18446744073709551615). Then the codebase will try to call `transaction_get_merkle` with the overflowed height. ### Notes to the reviewers The test introduced in this PR does not fail with the old codebase. What ends up happening is that `transaction_get_merke` will be called with the overflow height and the Electrum server will return with "merkle not found". To see the failure, you can change the `height as usize` casts to use `.try_into().expect()` then you will see a panic. These changes should be applied as `1.0.1` and `1.1.1` fixes. ### Changelog notice * Fix `bdk_electrum` handling of negative spk-history height. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### Bugfixes: * [x] I've added tests to reproduce the issue which are now passing ~* [ ] This pull request breaks the existing API~ ~* [ ] I'm linking the issue being fixed by this PR~ ACKs for top commit: LagginTimes: ACK 161d715 Tree-SHA512: 18bc5c6ccebd810e253cc88a69554ef6a27168df1961a5d77e2044bf30f4e27d44cb3a2499f5d29f0a3bd0eb349cacd7ea870f9c7225dd09798c7e19091a0f04
2 parents a41db82 + 161d715 commit ad21685

File tree

2 files changed

+78
-7
lines changed

2 files changed

+78
-7
lines changed

crates/electrum/src/bdk_electrum_client.rs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,9 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
276276

277277
for tx_res in spk_history {
278278
tx_update.txs.push(self.fetch_tx(tx_res.tx_hash)?);
279-
self.validate_merkle_for_anchor(tx_update, tx_res.tx_hash, tx_res.height)?;
279+
if let Ok(height) = tx_res.height.try_into() {
280+
self.validate_merkle_for_anchor(tx_update, tx_res.tx_hash, height)?;
281+
}
280282
}
281283
}
282284
}
@@ -312,7 +314,9 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
312314
if !has_residing && res.tx_hash == op_txid {
313315
has_residing = true;
314316
tx_update.txs.push(Arc::clone(&op_tx));
315-
self.validate_merkle_for_anchor(tx_update, res.tx_hash, res.height)?;
317+
if let Ok(height) = res.height.try_into() {
318+
self.validate_merkle_for_anchor(tx_update, res.tx_hash, height)?;
319+
}
316320
}
317321

318322
if !has_spending && res.tx_hash != op_txid {
@@ -326,7 +330,9 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
326330
continue;
327331
}
328332
tx_update.txs.push(Arc::clone(&res_tx));
329-
self.validate_merkle_for_anchor(tx_update, res.tx_hash, res.height)?;
333+
if let Ok(height) = res.height.try_into() {
334+
self.validate_merkle_for_anchor(tx_update, res.tx_hash, height)?;
335+
}
330336
}
331337
}
332338
}
@@ -360,7 +366,9 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
360366
.into_iter()
361367
.find(|r| r.tx_hash == txid)
362368
{
363-
self.validate_merkle_for_anchor(tx_update, txid, r.height)?;
369+
if let Ok(height) = r.height.try_into() {
370+
self.validate_merkle_for_anchor(tx_update, txid, height)?;
371+
}
364372
}
365373

366374
tx_update.txs.push(tx);
@@ -374,11 +382,11 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
374382
&self,
375383
tx_update: &mut TxUpdate<ConfirmationBlockTime>,
376384
txid: Txid,
377-
confirmation_height: i32,
385+
confirmation_height: usize,
378386
) -> Result<(), Error> {
379387
if let Ok(merkle_res) = self
380388
.inner
381-
.transaction_get_merkle(&txid, confirmation_height as usize)
389+
.transaction_get_merkle(&txid, confirmation_height)
382390
{
383391
let mut header = self.fetch_header(merkle_res.block_height as u32)?;
384392
let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof(

crates/electrum/tests/test_electrum.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ use bdk_chain::{
55
spk_txout::SpkTxOutIndex,
66
Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph,
77
};
8+
use bdk_core::bitcoin::Network;
89
use bdk_electrum::BdkElectrumClient;
9-
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
10+
use bdk_testenv::{
11+
anyhow,
12+
bitcoincore_rpc::{json::CreateRawTransactionInput, RawTx, RpcApi},
13+
TestEnv,
14+
};
1015
use core::time::Duration;
16+
use electrum_client::ElectrumApi;
1117
use std::collections::{BTreeSet, HashSet};
1218
use std::str::FromStr;
1319

@@ -54,6 +60,63 @@ where
5460
Ok(update)
5561
}
5662

63+
/// If an spk history contains a tx that spends another unconfirmed tx (chained mempool history),
64+
/// the Electrum API will return the tx with a negative height. This should succeed and not panic.
65+
#[test]
66+
pub fn chained_mempool_tx_sync() -> anyhow::Result<()> {
67+
let env = TestEnv::new()?;
68+
let rpc_client = env.rpc_client();
69+
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
70+
71+
let tracked_addr = rpc_client
72+
.get_new_address(None, None)?
73+
.require_network(Network::Regtest)?;
74+
75+
env.mine_blocks(100, None)?;
76+
77+
// First unconfirmed tx.
78+
env.send(&tracked_addr, Amount::from_btc(1.0)?)?;
79+
80+
// Create second unconfirmed tx that spends the first.
81+
let utxo = rpc_client
82+
.list_unspent(None, Some(0), None, Some(true), None)?
83+
.into_iter()
84+
.find(|utxo| utxo.script_pub_key == tracked_addr.script_pubkey())
85+
.expect("must find the newly created utxo");
86+
let tx_that_spends_unconfirmed = rpc_client.create_raw_transaction(
87+
&[CreateRawTransactionInput {
88+
txid: utxo.txid,
89+
vout: utxo.vout,
90+
sequence: None,
91+
}],
92+
&[(
93+
tracked_addr.to_string(),
94+
utxo.amount - Amount::from_sat(1000),
95+
)]
96+
.into(),
97+
None,
98+
None,
99+
)?;
100+
let signed_tx = rpc_client
101+
.sign_raw_transaction_with_wallet(tx_that_spends_unconfirmed.raw_hex(), None, None)?
102+
.transaction()?;
103+
rpc_client.send_raw_transaction(signed_tx.raw_hex())?;
104+
105+
env.wait_until_electrum_sees_txid(signed_tx.compute_txid(), Duration::from_secs(5))?;
106+
107+
let spk_history = electrum_client.script_get_history(&tracked_addr.script_pubkey())?;
108+
assert!(
109+
spk_history.into_iter().any(|tx_res| tx_res.height < 0),
110+
"must find tx with negative height"
111+
);
112+
113+
let client = BdkElectrumClient::new(electrum_client);
114+
let request = SyncRequest::builder().spks(core::iter::once(tracked_addr.script_pubkey()));
115+
let _response = client.sync(request, 1, false)?;
116+
117+
Ok(())
118+
}
119+
57120
#[test]
58121
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
59122
let env = TestEnv::new()?;

0 commit comments

Comments
 (0)