Merge bitcoindevkit/bdk#1416: [chain] Change tx_last_seen to Option<u64>

af75817d4b ref(tx_graph): Change last_seen to `HashMap<Txid, u64>` (valued mammal)
6204d2c766 feat(tx_graph): Add method `txs_with_no_anchor_or_last_seen` (valued mammal)
496601b8b1 test(tx_graph): Add test for `list_canonical_txs` (valued mammal)
c4057297a9 wallet: delete method `insert_anchor` (valued mammal)
b34790c6b6 ref(tx_graph)!: Rename `list_chain_txs` to `list_canonical_txs` (valued mammal)
2ce4bb4dfc test(indexed_tx_graph): Add test_get_chain_position (valued mammal)
36f58870cb test(wallet): Add test_insert_tx_balance_and_utxos (valued mammal)
bbc19c3536 fix(tx_graph)!: Change tx_last_seen to `Option<u64>` (valued mammal)
324eeb3eb4 fix(wallet)!: Rework `Wallet::insert_tx` to no longer insert anchors (valued mammal)

Pull request description:

  The PR changes the type of last_seen to `Option<u64>` for `txs` member of `TxGraph`.

  This fixes an issue where unbroadcast and otherwise non-canonical transactions were returned from methods `list_chain_txs` and `Wallet::transactions` because every new tx inserted had a last_seen of 0 making it appear unconfirmed.

  fixes #1446
  fixes #1396

  ### Notes to the reviewers

  ### Changelog notice

  Changed
  - Member `last_seen_unconfirmed` of `TxNode` is changed to `Option<u64>`
  - Renamed `TxGraph` method `list_chain_txs` to `list_canonical_txs`
  - Changed `Wallet::insert_tx` to take a single `tx: Transaction` as parameter

  Added
  - Add method `txs_with_no_anchor_or_last_seen` for `TxGraph`
  - Add method `unbroadcast_transactions` for `Wallet`

  ### 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] This pull request breaks the existing API
  * [x] I've added tests to reproduce the issue which are now passing
  * [x] I'm linking the issue being fixed by this PR

ACKs for top commit:
  notmandatory:
    Re ACK af75817d4b

Tree-SHA512: e664b3b49e2f547873923f15dffbbc7fa032b6240e5b856b180e9e26123ca141864d10448912dc4a31bbb200c75bef4251a910a4330dac17ee6841b564612d13
This commit is contained in:
Steve Myers
2024-07-02 16:43:42 -05:00
13 changed files with 566 additions and 346 deletions

View File

@@ -1,7 +1,8 @@
#![allow(unused)]
use bdk_chain::indexed_tx_graph::Indexer;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor, TxGraph};
use bdk_wallet::wallet::Update;
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
use bitcoin::hashes::Hash;
use bitcoin::{
@@ -77,24 +78,26 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_tx(
tx0,
ConfirmationTime::Confirmed {
height: 1_000,
time: 100,
},
)
.unwrap();
wallet
.insert_tx(
tx1.clone(),
ConfirmationTime::Confirmed {
height: 2_000,
time: 200,
},
)
.unwrap();
wallet.insert_tx(tx0.clone());
insert_anchor_from_conf(
&mut wallet,
tx0.compute_txid(),
ConfirmationTime::Confirmed {
height: 1_000,
time: 100,
},
);
wallet.insert_tx(tx1.clone());
insert_anchor_from_conf(
&mut wallet,
tx1.compute_txid(),
ConfirmationTime::Confirmed {
height: 2_000,
time: 200,
},
);
(wallet, tx1.compute_txid())
}
@@ -192,3 +195,31 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
FeeRate::from_sat_per_kwu(sat_kwu)
}
/// Simulates confirming a tx with `txid` at the specified `position` by inserting an anchor
/// at the lowest height in local chain that is greater or equal to `position`'s height,
/// assuming the confirmation time matches `ConfirmationTime::Confirmed`.
pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: ConfirmationTime) {
if let ConfirmationTime::Confirmed { height, time } = position {
// anchor tx to checkpoint with lowest height that is >= position's height
let anchor = wallet
.local_chain()
.range(height..)
.last()
.map(|anchor_cp| ConfirmationTimeHeightAnchor {
anchor_block: anchor_cp.block_id(),
confirmation_height: height,
confirmation_time: time,
})
.expect("confirmation height cannot be greater than tip");
let mut graph = TxGraph::default();
let _ = graph.insert_anchor(txid, anchor);
wallet
.apply_update(Update {
graph,
..Default::default()
})
.unwrap();
}
}

View File

@@ -51,12 +51,19 @@ fn receive_output_to_address(
}],
};
wallet.insert_tx(tx.clone(), height).unwrap();
let txid = tx.compute_txid();
wallet.insert_tx(tx);
OutPoint {
txid: tx.compute_txid(),
vout: 0,
match height {
ConfirmationTime::Confirmed { .. } => {
insert_anchor_from_conf(wallet, txid, height);
}
ConfirmationTime::Unconfirmed { last_seen } => {
insert_seen_at(wallet, txid, last_seen);
}
}
OutPoint { txid, vout: 0 }
}
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
@@ -70,6 +77,18 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
receive_output(wallet, value, anchor)
}
fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) {
use bdk_wallet::wallet::Update;
let mut graph = bdk_chain::TxGraph::default();
let _ = graph.insert_seen_at(txid, seen_at);
wallet
.apply_update(Update {
graph,
..Default::default()
})
.unwrap();
}
// The satisfaction size of a P2WPKH is 112 WU =
// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len)
// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for
@@ -1188,12 +1207,16 @@ fn test_create_tx_add_utxo() {
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
wallet
.insert_tx(
small_output_tx.clone(),
ConfirmationTime::Unconfirmed { last_seen: 0 },
)
.unwrap();
let txid = small_output_tx.compute_txid();
wallet.insert_tx(small_output_tx);
insert_anchor_from_conf(
&mut wallet,
txid,
ConfirmationTime::Confirmed {
height: 2000,
time: 200,
},
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
@@ -1201,10 +1224,7 @@ fn test_create_tx_add_utxo() {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
.add_utxo(OutPoint {
txid: small_output_tx.compute_txid(),
vout: 0,
})
.add_utxo(OutPoint { txid, vout: 0 })
.unwrap();
let psbt = builder.finish().unwrap();
let sent_received =
@@ -1237,13 +1257,16 @@ fn test_create_tx_manually_selected_insufficient() {
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
wallet
.insert_tx(
small_output_tx.clone(),
ConfirmationTime::Unconfirmed { last_seen: 0 },
)
.unwrap();
let txid = small_output_tx.compute_txid();
wallet.insert_tx(small_output_tx.clone());
insert_anchor_from_conf(
&mut wallet,
txid,
ConfirmationTime::Confirmed {
height: 2000,
time: 200,
},
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
@@ -1251,10 +1274,7 @@ fn test_create_tx_manually_selected_insufficient() {
let mut builder = wallet.build_tx();
builder
.add_recipient(addr.script_pubkey(), Amount::from_sat(30_000))
.add_utxo(OutPoint {
txid: small_output_tx.compute_txid(),
vout: 0,
})
.add_utxo(OutPoint { txid, vout: 0 })
.unwrap()
.manually_selected_only();
builder.finish().unwrap();
@@ -1289,9 +1309,9 @@ fn test_create_tx_policy_path_no_csv() {
value: Amount::from_sat(50_000),
}],
};
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
let txid = tx.compute_txid();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
@@ -1659,9 +1679,8 @@ fn test_bump_fee_irreplaceable_tx() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
@@ -1677,15 +1696,15 @@ fn test_bump_fee_confirmed_tx() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(
tx,
ConfirmationTime::Confirmed {
height: 42,
time: 42_000,
},
)
.unwrap();
wallet.insert_tx(tx);
insert_anchor_from_conf(
&mut wallet,
txid,
ConfirmationTime::Confirmed {
height: 42,
time: 42_000,
},
);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
@@ -1704,9 +1723,8 @@ fn test_bump_fee_low_fee_rate() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::BROADCAST_MIN);
@@ -1737,9 +1755,8 @@ fn test_bump_fee_low_abs() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(10));
@@ -1759,9 +1776,8 @@ fn test_bump_fee_zero_abs() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::ZERO);
@@ -1785,9 +1801,8 @@ fn test_bump_fee_reduce_change() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
@@ -1883,9 +1898,8 @@ fn test_bump_fee_reduce_single_recipient() {
let original_sent_received = wallet.sent_and_received(&tx);
let original_fee = check_fee!(wallet, psbt);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
@@ -1931,9 +1945,8 @@ fn test_bump_fee_absolute_reduce_single_recipient() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
@@ -1976,15 +1989,18 @@ fn test_bump_fee_drain_wallet() {
value: Amount::from_sat(25_000),
}],
};
wallet
.insert_tx(
tx.clone(),
ConfirmationTime::Confirmed {
height: wallet.latest_checkpoint().height(),
time: 42_000,
},
)
.unwrap();
let txid = tx.compute_txid();
let tip = wallet.latest_checkpoint().height();
wallet.insert_tx(tx.clone());
insert_anchor_from_conf(
&mut wallet,
txid,
ConfirmationTime::Confirmed {
height: tip,
time: 42_000,
},
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
@@ -2004,9 +2020,8 @@ fn test_bump_fee_drain_wallet() {
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
// for the new feerate, it should be enough to reduce the output, but since we specify
@@ -2041,18 +2056,17 @@ fn test_bump_fee_remove_output_manually_selected_only() {
value: Amount::from_sat(25_000),
}],
};
wallet
.insert_tx(
init_tx.clone(),
wallet
.transactions()
.last()
.unwrap()
.chain_position
.cloned()
.into(),
)
.unwrap();
let position: ConfirmationTime = wallet
.transactions()
.last()
.unwrap()
.chain_position
.cloned()
.into();
wallet.insert_tx(init_tx.clone());
insert_anchor_from_conf(&mut wallet, init_tx.compute_txid(), position);
let outpoint = OutPoint {
txid: init_tx.compute_txid(),
vout: 0,
@@ -2071,9 +2085,8 @@ fn test_bump_fee_remove_output_manually_selected_only() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
let mut builder = wallet.build_fee_bump(txid).unwrap();
@@ -2097,14 +2110,16 @@ fn test_bump_fee_add_input() {
value: Amount::from_sat(25_000),
}],
};
let pos = wallet
let txid = init_tx.compute_txid();
let pos: ConfirmationTime = wallet
.transactions()
.last()
.unwrap()
.chain_position
.cloned()
.into();
wallet.insert_tx(init_tx, pos).unwrap();
wallet.insert_tx(init_tx);
insert_anchor_from_conf(&mut wallet, txid, pos);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
@@ -2117,9 +2132,8 @@ fn test_bump_fee_add_input() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_details = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
@@ -2174,9 +2188,8 @@ fn test_bump_fee_absolute_add_input() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(6_000));
@@ -2240,9 +2253,8 @@ fn test_bump_fee_no_change_add_input_and_change() {
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
// Now bump the fees, the wallet should add an extra input and a change output, and leave
// the original output untouched.
@@ -2310,9 +2322,8 @@ fn test_bump_fee_add_input_change_dust() {
assert_eq!(tx.input.len(), 1);
assert_eq!(tx.output.len(), 2);
let txid = tx.compute_txid();
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
// We set a fee high enough that during rbf we are forced to add
@@ -2381,9 +2392,8 @@ fn test_bump_fee_force_add_input() {
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx.clone());
insert_seen_at(&mut wallet, txid, 0);
// the new fee_rate is low enough that just reducing the change would be fine, but we force
// the addition of an extra input with `add_utxo()`
let mut builder = wallet.build_fee_bump(txid).unwrap();
@@ -2448,9 +2458,8 @@ fn test_bump_fee_absolute_force_add_input() {
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx.clone());
insert_seen_at(&mut wallet, txid, 0);
// the new fee_rate is low enough that just reducing the change would be fine, but we force
// the addition of an extra input with `add_utxo()`
@@ -2527,9 +2536,8 @@ fn test_bump_fee_unconfirmed_inputs_only() {
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
builder.finish().unwrap();
@@ -2560,9 +2568,8 @@ fn test_bump_fee_unconfirmed_input() {
for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
@@ -3755,15 +3762,16 @@ fn test_spend_coinbase() {
value: Amount::from_sat(25_000),
}],
};
wallet
.insert_tx(
coinbase_tx,
ConfirmationTime::Confirmed {
height: confirmation_height,
time: 30_000,
},
)
.unwrap();
let txid = coinbase_tx.compute_txid();
wallet.insert_tx(coinbase_tx);
insert_anchor_from_conf(
&mut wallet,
txid,
ConfirmationTime::Confirmed {
height: confirmation_height,
time: 30_000,
},
);
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
let maturity_time = confirmation_height + COINBASE_MATURITY;
@@ -4092,3 +4100,45 @@ fn test_thread_safety() {
fn thread_safe<T: Send + Sync>() {}
thread_safe::<Wallet>(); // compiles only if true
}
#[test]
fn test_insert_tx_balance_and_utxos() {
// creating many txs has no effect on the wallet's available utxos
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")
.unwrap()
.assume_checked();
let unspent: Vec<_> = wallet.list_unspent().collect();
assert!(!unspent.is_empty());
let balance = wallet.balance().total();
let fee = Amount::from_sat(143);
let amt = balance - fee;
for _ in 0..3 {
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), amt);
let mut psbt = builder.finish().unwrap();
assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap());
let tx = psbt.extract_tx().unwrap();
let _ = wallet.insert_tx(tx);
}
assert_eq!(wallet.list_unspent().collect::<Vec<_>>(), unspent);
assert_eq!(wallet.balance().confirmed, balance);
// manually setting a tx last_seen will consume the wallet's available utxos
let addr = Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")
.unwrap()
.assume_checked();
let mut builder = wallet.build_tx();
builder.add_recipient(addr.script_pubkey(), amt);
let mut psbt = builder.finish().unwrap();
assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap());
let tx = psbt.extract_tx().unwrap();
let txid = tx.compute_txid();
let _ = wallet.insert_tx(tx);
insert_seen_at(&mut wallet, txid, 2);
assert!(wallet.list_unspent().next().is_none());
assert_eq!(wallet.balance().total().to_sat(), 0);
}