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

@@ -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};
@@ -222,6 +222,8 @@ mod test {
use crate::wallet::Wallet;
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
use crate::wallet::Update;
use bdk_chain::TxGraph;
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
let transaction = Transaction {
input: vec![],
@@ -229,22 +231,27 @@ mod test {
version: transaction::Version::non_standard(0),
lock_time: bitcoin::absolute::LockTime::ZERO,
};
let txid = transaction.compute_txid();
let block_id = BlockId {
height: 5001,
hash: BlockHash::all_zeros(),
};
wallet.insert_checkpoint(block_id).unwrap();
wallet.insert_tx(transaction);
let anchor = ConfirmationTimeHeightAnchor {
confirmation_height: 5000,
confirmation_time: 0,
anchor_block: block_id,
};
let mut graph = TxGraph::default();
let _ = graph.insert_anchor(txid, anchor);
wallet
.insert_checkpoint(BlockId {
height: 5001,
hash: BlockHash::all_zeros(),
.apply_update(Update {
graph,
..Default::default()
})
.unwrap();
wallet
.insert_tx(
transaction,
ConfirmationTime::Confirmed {
height: 5000,
time: 0,
},
)
.unwrap();
wallet
}
#[test]

View File

@@ -27,7 +27,7 @@ use bdk_chain::{
self, ApplyHeaderError, CannotConnectError, CheckPoint, CheckPointIter, LocalChain,
},
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
tx_graph::{CanonicalTx, TxGraph},
tx_graph::{CanonicalTx, TxGraph, TxNode},
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
Indexed, IndexedTxGraph,
};
@@ -290,35 +290,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 {
@@ -1085,63 +1056,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.
@@ -1151,7 +1080,7 @@ impl Wallet {
{
self.indexed_graph
.graph()
.list_chain_txs(&self.chain, self.chain.tip().block_id())
.list_canonical_txs(&self.chain, self.chain.tip().block_id())
}
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
@@ -2326,6 +2255,14 @@ impl Wallet {
self.indexed_graph.graph()
}
/// Iterate over transactions in the wallet that are unseen and unanchored likely
/// because they haven't been broadcast.
pub fn unbroadcast_transactions(
&self,
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
self.tx_graph().txs_with_no_anchor_or_last_seen()
}
/// Get a reference to the inner [`KeychainTxOutIndex`].
pub fn spk_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
&self.indexed_graph.index
@@ -2545,8 +2482,9 @@ 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::{KeychainKind, wallet::Wallet};
use $crate::chain::{ConfirmationTimeHeightAnchor, BlockId, TxGraph};
use $crate::wallet::{Update, Wallet};
use $crate::KeychainKind;
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
@@ -2566,12 +2504,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);
let anchor = ConfirmationTimeHeightAnchor {
confirmation_height: 500,
confirmation_time: 50_000,
anchor_block: block,
};
let mut graph = TxGraph::default();
let _ = graph.insert_anchor(txid, anchor);
let update = Update { graph, ..Default::default() };
wallet.apply_update(update).unwrap();
wallet
}}
}