fix(wallet)!: Rework Wallet::insert_tx to no longer insert anchors

since we'd be lacking context that should normally occur during
sync with a chain source. The logic for inserting a graph
anchor from a `ConfirmationTime` is moved to the wallet common
test module in order to simulate receiving new txs and
confirming them.
This commit is contained in:
valued mammal 2024-05-20 07:01:05 -04:00
parent 6dab68d35b
commit 324eeb3eb4
No known key found for this signature in database
4 changed files with 176 additions and 233 deletions

View File

@ -214,7 +214,7 @@ mod test {
use core::str::FromStr;
use crate::std::string::ToString;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
use bitcoin::hashes::Hash;
use bitcoin::{transaction, BlockHash, Network, Transaction};
@ -229,21 +229,21 @@ mod test {
version: transaction::Version::non_standard(0),
lock_time: bitcoin::absolute::LockTime::ZERO,
};
wallet
.insert_checkpoint(BlockId {
let txid = transaction.compute_txid();
let block_id = BlockId {
height: 5001,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_tx(
transaction,
ConfirmationTime::Confirmed {
height: 5000,
time: 0,
};
wallet.insert_checkpoint(block_id).unwrap();
wallet.insert_tx(transaction);
wallet.insert_anchor(
txid,
ConfirmationTimeHeightAnchor {
confirmation_height: 5000,
confirmation_time: 0,
anchor_block: block_id,
},
)
.unwrap();
);
wallet
}

View File

@ -284,35 +284,6 @@ impl fmt::Display for NewOrLoadError {
#[cfg(feature = "std")]
impl std::error::Error for NewOrLoadError {}
/// An error that may occur when inserting a transaction into [`Wallet`].
#[derive(Debug)]
pub enum InsertTxError {
/// The error variant that occurs when the caller attempts to insert a transaction with a
/// confirmation height that is greater than the internal chain tip.
ConfirmationHeightCannotBeGreaterThanTip {
/// The internal chain's tip height.
tip_height: u32,
/// The introduced transaction's confirmation height.
tx_height: u32,
},
}
impl fmt::Display for InsertTxError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height,
tx_height,
} => {
write!(f, "cannot insert tx with confirmation height ({}) higher than internal tip height ({})", tx_height, tip_height)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for InsertTxError {}
/// An error that may occur when applying a block to [`Wallet`].
#[derive(Debug)]
pub enum ApplyBlockError {
@ -906,6 +877,23 @@ impl Wallet {
self.stage.append(additions.into());
}
/// Inserts an `anchor` for a transaction with the given `txid`.
///
/// This stages the changes, you must persist them later.
pub fn insert_anchor(&mut self, txid: Txid, anchor: ConfirmationTimeHeightAnchor) {
let indexed_graph_changeset = self.indexed_graph.insert_anchor(txid, anchor);
self.stage.append(indexed_graph_changeset.into());
}
/// Inserts a unix timestamp of when a transaction is seen in the mempool.
///
/// This is used for transaction conflict resolution where the transaction with the
/// later last-seen is prioritized. This stages the changes, you must persist them later.
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) {
let indexed_graph_changeset = self.indexed_graph.insert_seen_at(txid, seen_at);
self.stage.append(indexed_graph_changeset.into());
}
/// Calculates the fee of a given transaction. Returns [`Amount::ZERO`] if `tx` is a coinbase transaction.
///
/// To calculate the fee for a [`Transaction`] with inputs not owned by this wallet you must
@ -1079,63 +1067,21 @@ impl Wallet {
/// Add a transaction to the wallet's internal view of the chain. This stages the change,
/// you must persist it later.
///
/// Returns whether anything changed with the transaction insertion (e.g. `false` if the
/// transaction was already inserted at the same position).
/// This method inserts the given `tx` and returns whether anything changed after insertion,
/// which will be false if the same transaction already exists in the wallet's transaction
/// graph. Any changes are staged but not committed.
///
/// A `tx` can be rejected if `position` has a height greater than the [`latest_checkpoint`].
/// Therefore you should use [`insert_checkpoint`] to insert new checkpoints before manually
/// inserting new transactions.
/// # Note
///
/// **WARNING**: If `position` is confirmed, we anchor the `tx` to the lowest checkpoint that
/// is >= the `position`'s height. The caller is responsible for ensuring the `tx` exists in our
/// local view of the best chain's history.
///
/// You must persist the changes resulting from one or more calls to this method if you need
/// the inserted tx to be reloaded after closing the wallet.
///
/// [`commit`]: Self::commit
/// [`latest_checkpoint`]: Self::latest_checkpoint
/// [`insert_checkpoint`]: Self::insert_checkpoint
pub fn insert_tx(
&mut self,
tx: Transaction,
position: ConfirmationTime,
) -> Result<bool, InsertTxError> {
let (anchor, last_seen) = match position {
ConfirmationTime::Confirmed { height, time } => {
// anchor tx to checkpoint with lowest height that is >= position's height
let anchor = self
.chain
.range(height..)
.last()
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height: self.chain.tip().height(),
tx_height: height,
})
.map(|anchor_cp| ConfirmationTimeHeightAnchor {
anchor_block: anchor_cp.block_id(),
confirmation_height: height,
confirmation_time: time,
})?;
(Some(anchor), None)
}
ConfirmationTime::Unconfirmed { last_seen } => (None, Some(last_seen)),
};
/// By default the inserted `tx` won't be considered "canonical" because it's not known
/// whether the transaction exists in the best chain. To know whether it exists, the tx
/// must be broadcast to the network and the wallet synced via a chain source.
pub fn insert_tx(&mut self, tx: Transaction) -> bool {
let mut changeset = ChangeSet::default();
let txid = tx.compute_txid();
changeset.append(self.indexed_graph.insert_tx(tx).into());
if let Some(anchor) = anchor {
changeset.append(self.indexed_graph.insert_anchor(txid, anchor).into());
}
if let Some(last_seen) = last_seen {
changeset.append(self.indexed_graph.insert_seen_at(txid, last_seen).into());
}
let changed = !changeset.is_empty();
let ret = !changeset.is_empty();
self.stage.append(changeset);
Ok(changed)
ret
}
/// Iterate over the transactions in the wallet.
@ -2540,7 +2486,7 @@ macro_rules! floating_rate {
macro_rules! doctest_wallet {
() => {{
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
use $crate::chain::{ConfirmationTime, BlockId};
use $crate::chain::{ConfirmationTimeHeightAnchor, BlockId};
use $crate::{KeychainKind, wallet::Wallet};
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
@ -2561,11 +2507,19 @@ macro_rules! doctest_wallet {
script_pubkey: address.script_pubkey(),
}],
};
let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() });
let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed {
height: 500,
time: 50_000
});
let txid = tx.txid();
let block = BlockId { height: 1_000, hash: BlockHash::all_zeros() };
let _ = wallet.insert_checkpoint(block);
let _ = wallet.insert_tx(tx);
wallet
.insert_anchor(
txid,
ConfirmationTimeHeightAnchor {
confirmation_height: 500,
confirmation_time: 50_000,
anchor_block: block,
}
);
wallet
}}

View File

@ -1,7 +1,7 @@
#![allow(unused)]
use bdk_chain::indexed_tx_graph::Indexer;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_chain::{BlockId, ConfirmationTime, ConfirmationTimeHeightAnchor};
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
use bitcoin::hashes::Hash;
use bitcoin::{
@ -77,24 +77,26 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
hash: BlockHash::all_zeros(),
})
.unwrap();
wallet
.insert_tx(
tx0,
wallet.insert_tx(tx0.clone());
insert_anchor_from_conf(
&mut wallet,
tx0.compute_txid(),
ConfirmationTime::Confirmed {
height: 1_000,
time: 100,
},
)
.unwrap();
wallet
.insert_tx(
tx1.clone(),
);
wallet.insert_tx(tx1.clone());
insert_anchor_from_conf(
&mut wallet,
tx1.compute_txid(),
ConfirmationTime::Confirmed {
height: 2_000,
time: 200,
},
)
.unwrap();
);
(wallet, tx1.compute_txid())
}
@ -192,3 +194,24 @@ 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");
wallet.insert_anchor(txid, anchor);
}
}

View File

@ -42,12 +42,19 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: ConfirmationTime) ->
}],
};
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 } => {
wallet.insert_seen_at(txid, last_seen);
}
}
OutPoint { txid, vout: 0 }
}
fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint {
@ -1180,12 +1187,7 @@ 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();
wallet.insert_tx(small_output_tx.clone());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
@ -1230,12 +1232,7 @@ fn test_create_tx_manually_selected_insufficient() {
lock_time: absolute::LockTime::ZERO,
};
wallet
.insert_tx(
small_output_tx.clone(),
ConfirmationTime::Unconfirmed { last_seen: 0 },
)
.unwrap();
wallet.insert_tx(small_output_tx.clone());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
@ -1281,9 +1278,7 @@ fn test_create_tx_policy_path_no_csv() {
value: Amount::from_sat(50_000),
}],
};
wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed { last_seen: 0 })
.unwrap();
wallet.insert_tx(tx);
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
@ -1674,9 +1669,7 @@ 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);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
@ -1692,15 +1685,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,
wallet.insert_tx(tx);
insert_anchor_from_conf(
&mut wallet,
txid,
ConfirmationTime::Confirmed {
height: 42,
time: 42_000,
},
)
.unwrap();
);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
@ -1719,9 +1712,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::BROADCAST_MIN);
@ -1752,9 +1743,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(10));
@ -1774,9 +1763,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::ZERO);
@ -1800,9 +1787,7 @@ 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);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
@ -1898,9 +1883,7 @@ 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);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
@ -1946,9 +1929,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
@ -1991,15 +1972,18 @@ fn test_bump_fee_drain_wallet() {
value: Amount::from_sat(25_000),
}],
};
wallet
.insert_tx(
tx.clone(),
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: wallet.latest_checkpoint().height(),
height: tip,
time: 42_000,
},
)
.unwrap();
);
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
.unwrap()
.assume_checked();
@ -2019,9 +2003,7 @@ 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);
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
@ -2056,18 +2038,17 @@ fn test_bump_fee_remove_output_manually_selected_only() {
value: Amount::from_sat(25_000),
}],
};
wallet
.insert_tx(
init_tx.clone(),
wallet
let position: ConfirmationTime = wallet
.transactions()
.last()
.unwrap()
.chain_position
.cloned()
.into(),
)
.unwrap();
.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,
@ -2086,9 +2067,7 @@ 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);
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
let mut builder = wallet.build_fee_bump(txid).unwrap();
@ -2112,14 +2091,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()
@ -2132,9 +2113,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
@ -2189,9 +2168,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(6_000));
@ -2255,9 +2232,7 @@ 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);
// Now bump the fees, the wallet should add an extra input and a change output, and leave
// the original output untouched.
@ -2325,9 +2300,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
// We set a fee high enough that during rbf we are forced to add
@ -2396,9 +2369,7 @@ 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());
// 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();
@ -2463,9 +2434,7 @@ 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());
// 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()`
@ -2542,9 +2511,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
builder.finish().unwrap();
@ -2575,9 +2542,7 @@ 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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
@ -3774,15 +3739,16 @@ fn test_spend_coinbase() {
value: Amount::from_sat(25_000),
}],
};
wallet
.insert_tx(
coinbase_tx,
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,
},
)
.unwrap();
);
let not_yet_mature_time = confirmation_height + COINBASE_MATURITY - 1;
let maturity_time = confirmation_height + COINBASE_MATURITY;