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

@@ -109,10 +109,11 @@ use core::{
/// [module-level documentation]: crate::tx_graph
#[derive(Clone, Debug, PartialEq)]
pub struct TxGraph<A = ()> {
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)`
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, u64)>,
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors)`
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>)>,
spends: BTreeMap<OutPoint, HashSet<Txid>>,
anchors: BTreeSet<(A, Txid)>,
last_seen: HashMap<Txid, u64>,
// This atrocity exists so that `TxGraph::outspends()` can return a reference.
// FIXME: This can be removed once `HashSet::new` is a const fn.
@@ -125,6 +126,7 @@ impl<A> Default for TxGraph<A> {
txs: Default::default(),
spends: Default::default(),
anchors: Default::default(),
last_seen: Default::default(),
empty_outspends: Default::default(),
}
}
@@ -140,7 +142,7 @@ pub struct TxNode<'a, T, A> {
/// The blocks that the transaction is "anchored" in.
pub anchors: &'a BTreeSet<A>,
/// The last-seen unix timestamp of the transaction as unconfirmed.
pub last_seen_unconfirmed: u64,
pub last_seen_unconfirmed: Option<u64>,
}
impl<'a, T, A> Deref for TxNode<'a, T, A> {
@@ -210,7 +212,7 @@ impl<A> TxGraph<A> {
///
/// This includes txouts of both full transactions as well as floating transactions.
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
self.txs.iter().flat_map(|(txid, (tx, _))| match tx {
TxNodeInternal::Whole(tx) => tx
.as_ref()
.output
@@ -232,7 +234,7 @@ impl<A> TxGraph<A> {
pub fn floating_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
self.txs
.iter()
.filter_map(|(txid, (tx_node, _, _))| match tx_node {
.filter_map(|(txid, (tx_node, _))| match tx_node {
TxNodeInternal::Whole(_) => None,
TxNodeInternal::Partial(txouts) => Some(
txouts
@@ -247,17 +249,30 @@ impl<A> TxGraph<A> {
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
self.txs
.iter()
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
.filter_map(|(&txid, (tx, anchors))| match tx {
TxNodeInternal::Whole(tx) => Some(TxNode {
txid,
tx: tx.clone(),
anchors,
last_seen_unconfirmed: *last_seen,
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
}),
TxNodeInternal::Partial(_) => None,
})
}
/// Iterate over graph transactions with no anchors or last-seen.
pub fn txs_with_no_anchor_or_last_seen(
&self,
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
self.full_txs().filter_map(|tx| {
if tx.anchors.is_empty() && tx.last_seen_unconfirmed.is_none() {
Some(tx)
} else {
None
}
})
}
/// Get a transaction by txid. This only returns `Some` for full transactions.
///
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
@@ -270,11 +285,11 @@ impl<A> TxGraph<A> {
/// Get a transaction node by txid. This only returns `Some` for full transactions.
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
match &self.txs.get(&txid)? {
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
(TxNodeInternal::Whole(tx), anchors) => Some(TxNode {
txid,
tx: tx.clone(),
anchors,
last_seen_unconfirmed: *last_seen,
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
}),
_ => None,
}
@@ -504,7 +519,6 @@ impl<A: Clone + Ord> TxGraph<A> {
(
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
BTreeSet::new(),
0,
),
);
self.apply_update(update)
@@ -518,7 +532,7 @@ impl<A: Clone + Ord> TxGraph<A> {
let mut update = Self::default();
update.txs.insert(
tx.compute_txid(),
(TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
(TxNodeInternal::Whole(tx), BTreeSet::new()),
);
self.apply_update(update)
}
@@ -559,8 +573,7 @@ impl<A: Clone + Ord> TxGraph<A> {
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
let mut update = Self::default();
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
*update_last_seen = seen_at;
update.last_seen.insert(txid, seen_at);
self.apply_update(update)
}
@@ -607,7 +620,7 @@ impl<A: Clone + Ord> TxGraph<A> {
.txs
.iter()
.filter_map(
|(&txid, (_, anchors, _))| {
|(&txid, (_, anchors))| {
if anchors.is_empty() {
Some(txid)
} else {
@@ -656,10 +669,10 @@ impl<A: Clone + Ord> TxGraph<A> {
});
match self.txs.get_mut(&txid) {
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
Some((tx_node @ TxNodeInternal::Partial(_), _)) => {
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
}
Some((TxNodeInternal::Whole(tx), _, _)) => {
Some((TxNodeInternal::Whole(tx), _)) => {
debug_assert_eq!(
tx.as_ref().compute_txid(),
txid,
@@ -667,10 +680,8 @@ impl<A: Clone + Ord> TxGraph<A> {
);
}
None => {
self.txs.insert(
txid,
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
);
self.txs
.insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new()));
}
}
}
@@ -679,9 +690,8 @@ impl<A: Clone + Ord> TxGraph<A> {
let tx_entry = self.txs.entry(outpoint.txid).or_default();
match tx_entry {
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
}
(TxNodeInternal::Partial(txouts), _, _) => {
(TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ }
(TxNodeInternal::Partial(txouts), _) => {
txouts.insert(outpoint.vout, txout);
}
}
@@ -689,13 +699,13 @@ impl<A: Clone + Ord> TxGraph<A> {
for (anchor, txid) in changeset.anchors {
if self.anchors.insert((anchor.clone(), txid)) {
let (_, anchors, _) = self.txs.entry(txid).or_default();
let (_, anchors) = self.txs.entry(txid).or_default();
anchors.insert(anchor);
}
}
for (txid, new_last_seen) in changeset.last_seen {
let (_, _, last_seen) = self.txs.entry(txid).or_default();
let last_seen = self.last_seen.entry(txid).or_default();
if new_last_seen > *last_seen {
*last_seen = new_last_seen;
}
@@ -709,11 +719,10 @@ impl<A: Clone + Ord> TxGraph<A> {
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
let mut changeset = ChangeSet::<A>::default();
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
for (&txid, (update_tx_node, _)) in &update.txs {
match (self.txs.get(&txid), update_tx_node) {
(None, TxNodeInternal::Whole(update_tx)) => {
changeset.txs.insert(update_tx.clone());
0
}
(None, TxNodeInternal::Partial(update_txos)) => {
changeset.txouts.extend(
@@ -721,18 +730,13 @@ impl<A: Clone + Ord> TxGraph<A> {
.iter()
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
);
0
}
(Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen,
(
Some((TxNodeInternal::Partial(_), _, last_seen)),
TxNodeInternal::Whole(update_tx),
) => {
(Some((TxNodeInternal::Whole(_), _)), _) => {}
(Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => {
changeset.txs.insert(update_tx.clone());
*last_seen
}
(
Some((TxNodeInternal::Partial(txos), _, last_seen)),
Some((TxNodeInternal::Partial(txos), _)),
TxNodeInternal::Partial(update_txos),
) => {
changeset.txouts.extend(
@@ -741,12 +745,14 @@ impl<A: Clone + Ord> TxGraph<A> {
.filter(|(vout, _)| !txos.contains_key(*vout))
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
);
*last_seen
}
};
}
}
if *update_last_seen > prev_last_seen {
changeset.last_seen.insert(txid, *update_last_seen);
for (txid, update_last_seen) in update.last_seen {
let prev_last_seen = self.last_seen.get(&txid).copied();
if Some(update_last_seen) > prev_last_seen {
changeset.last_seen.insert(txid, update_last_seen);
}
}
@@ -786,7 +792,7 @@ impl<A: Anchor> TxGraph<A> {
chain_tip: BlockId,
txid: Txid,
) -> Result<Option<ChainPosition<&A>>, C::Error> {
let (tx_node, anchors, last_seen) = match self.txs.get(&txid) {
let (tx_node, anchors) = match self.txs.get(&txid) {
Some(v) => v,
None => return Ok(None),
};
@@ -798,6 +804,13 @@ impl<A: Anchor> TxGraph<A> {
}
}
// If no anchors are in best chain and we don't have a last_seen, we can return
// early because by definition the tx doesn't have a chain position.
let last_seen = match self.last_seen.get(&txid) {
Some(t) => *t,
None => return Ok(None),
};
// The tx is not anchored to a block in the best chain, which means that it
// might be in mempool, or it might have been dropped already.
// Let's check conflicts to find out!
@@ -884,7 +897,7 @@ impl<A: Anchor> TxGraph<A> {
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
return Ok(None);
}
if conflicting_tx.last_seen_unconfirmed == *last_seen
if conflicting_tx.last_seen_unconfirmed == Some(last_seen)
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
{
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
@@ -893,7 +906,7 @@ impl<A: Anchor> TxGraph<A> {
}
}
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
Ok(Some(ChainPosition::Unconfirmed(last_seen)))
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
@@ -971,10 +984,10 @@ impl<A: Anchor> TxGraph<A> {
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
/// returned item.
///
/// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead.
/// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
///
/// [`list_chain_txs`]: Self::list_chain_txs
pub fn try_list_chain_txs<'a, C: ChainOracle + 'a>(
/// [`list_canonical_txs`]: Self::list_canonical_txs
pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
&'a self,
chain: &'a C,
chain_tip: BlockId,
@@ -993,15 +1006,15 @@ impl<A: Anchor> TxGraph<A> {
/// List graph transactions that are in `chain` with `chain_tip`.
///
/// This is the infallible version of [`try_list_chain_txs`].
/// This is the infallible version of [`try_list_canonical_txs`].
///
/// [`try_list_chain_txs`]: Self::try_list_chain_txs
pub fn list_chain_txs<'a, C: ChainOracle + 'a>(
/// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
pub fn list_canonical_txs<'a, C: ChainOracle + 'a>(
&'a self,
chain: &'a C,
chain_tip: BlockId,
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
self.try_list_chain_txs(chain, chain_tip)
self.try_list_canonical_txs(chain, chain_tip)
.map(|r| r.expect("oracle is infallible"))
}

View File

@@ -131,9 +131,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
for anchor in tx_tmp.anchors.iter() {
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
}
if let Some(seen_at) = tx_tmp.last_seen {
let _ = graph.insert_seen_at(tx.compute_txid(), seen_at);
}
let _ = graph.insert_seen_at(tx.compute_txid(), tx_tmp.last_seen.unwrap_or(0));
}
(graph, spk_index, tx_ids)
}

View File

@@ -116,8 +116,8 @@ fn insert_relevant_txs() {
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
/// tx5: Mempool tx, sending 15000 sats to "untested" address.
/// tx4: Mempool tx, sending 20000 sats to "untrusted" address.
/// tx5: Mempool tx, sending 15000 sats to "trusted" address.
/// tx6: Complete unrelated tx. [Block 3]
///
/// Different transactions are added via `insert_relevant_txs`.
@@ -160,7 +160,7 @@ fn test_list_owned_txouts() {
let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
{
// we need to scope here to take immutanble reference of the graph
// we need to scope here to take immutable reference of the graph
for _ in 0..10 {
let ((_, script), _) = graph
.index
@@ -226,7 +226,7 @@ fn test_list_owned_txouts() {
..common::new_tx(0)
};
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
// tx5 is an external transaction receiving at trusted keychain, unconfirmed.
let tx5 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(15000),
@@ -239,7 +239,7 @@ fn test_list_owned_txouts() {
let tx6 = common::new_tx(0);
// Insert transactions into graph with respective anchors
// For unconfirmed txs we pass in `None`.
// Insert unconfirmed txs with a last_seen timestamp
let _ =
graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
@@ -291,9 +291,6 @@ fn test_list_owned_txouts() {
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
);
assert_eq!(txouts.len(), 5);
assert_eq!(utxos.len(), 4);
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
@@ -359,29 +356,25 @@ fn test_list_owned_txouts() {
balance,
) = fetch(0, &graph);
// tx1 is a confirmed txout and is unspent
// tx4, tx5 are unconfirmed
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
assert_eq!(
unconfirmed_txouts_txid,
[
tx2.compute_txid(),
tx3.compute_txid(),
tx4.compute_txid(),
tx5.compute_txid()
]
.into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
assert_eq!(
unconfirmed_utxos_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
}
@@ -405,23 +398,26 @@ fn test_list_owned_txouts() {
);
assert_eq!(
unconfirmed_txouts_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
// tx2 doesn't get into confirmed utxos set
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
// tx2 gets into confirmed utxos set
assert_eq!(
confirmed_utxos_txid,
[tx1.compute_txid(), tx2.compute_txid()].into()
);
assert_eq!(
unconfirmed_utxos_txid,
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
[tx4.compute_txid(), tx5.compute_txid()].into()
);
assert_eq!(
balance,
Balance {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::ZERO // Nothing is confirmed yet
confirmed: Amount::from_sat(30_000) // tx2 got confirmed
}
);
}
@@ -477,6 +473,7 @@ fn test_list_owned_txouts() {
balance,
) = fetch(98, &graph);
// no change compared to block 2
assert_eq!(
confirmed_txouts_txid,
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
@@ -502,14 +499,14 @@ fn test_list_owned_txouts() {
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
confirmed: Amount::from_sat(10000) // tx1 got matured
confirmed: Amount::from_sat(10000) // tx3 is confirmed
}
);
}
// AT Block 99
{
let (_, _, _, _, balance) = fetch(100, &graph);
let (_, _, _, _, balance) = fetch(99, &graph);
// Coinbase maturity hits
assert_eq!(
@@ -523,3 +520,147 @@ fn test_list_owned_txouts() {
);
}
}
/// Given a `LocalChain`, `IndexedTxGraph`, and a `Transaction`, when we insert some anchor
/// (possibly non-canonical) and/or a last-seen timestamp into the graph, we expect the
/// result of `get_chain_position` in these cases:
///
/// - tx with no anchors or last_seen has no `ChainPosition`
/// - tx with any last_seen will be `Unconfirmed`
/// - tx with an anchor in best chain will be `Confirmed`
/// - tx with an anchor not in best chain (no last_seen) has no `ChainPosition`
#[test]
fn test_get_chain_position() {
use bdk_chain::local_chain::CheckPoint;
use bdk_chain::BlockId;
use bdk_chain::SpkTxOutIndex;
struct TestCase<A> {
name: &'static str,
tx: Transaction,
anchor: Option<A>,
last_seen: Option<u64>,
exp_pos: Option<ChainPosition<A>>,
}
// addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm
let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap();
let mut graph = IndexedTxGraph::new({
let mut index = SpkTxOutIndex::default();
let _ = index.insert_spk(0u32, spk.clone());
index
});
// Anchors to test
let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")];
let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap();
let chain = LocalChain::from_tip(cp).unwrap();
// The test will insert a transaction into the indexed tx graph
// along with any anchors and timestamps, then check the value
// returned by `get_chain_position`.
fn run(
chain: &LocalChain,
graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<u32>>,
test: TestCase<BlockId>,
) {
let TestCase {
name,
tx,
anchor,
last_seen,
exp_pos,
} = test;
// add data to graph
let txid = tx.compute_txid();
let _ = graph.insert_tx(tx);
if let Some(anchor) = anchor {
let _ = graph.insert_anchor(txid, anchor);
}
if let Some(seen_at) = last_seen {
let _ = graph.insert_seen_at(txid, seen_at);
}
// check chain position
let res = graph
.graph()
.get_chain_position(chain, chain.tip().block_id(), txid);
assert_eq!(
res.map(ChainPosition::cloned),
exp_pos,
"failed test case: {name}"
);
}
[
TestCase {
name: "tx no anchors or last_seen - no chain pos",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(0)
},
anchor: None,
last_seen: None,
exp_pos: None,
},
TestCase {
name: "tx last_seen - unconfirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(1)
},
anchor: None,
last_seen: Some(2),
exp_pos: Some(ChainPosition::Unconfirmed(2)),
},
TestCase {
name: "tx anchor in best chain - confirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(2)
},
anchor: Some(blocks[1]),
last_seen: None,
exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
},
TestCase {
name: "tx unknown anchor with last_seen - unconfirmed",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(3)
},
anchor: Some(block_id!(2, "B'")),
last_seen: Some(2),
exp_pos: Some(ChainPosition::Unconfirmed(2)),
},
TestCase {
name: "tx unknown anchor - no chain pos",
tx: Transaction {
output: vec![TxOut {
value: Amount::ONE_BTC,
script_pubkey: spk.clone(),
}],
..common::new_tx(4)
},
anchor: Some(block_id!(2, "B'")),
last_seen: None,
exp_pos: None,
},
]
.into_iter()
.for_each(|t| run(&chain, &mut graph, t));
}

View File

@@ -977,16 +977,6 @@ fn test_chain_spends() {
}))
);
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
assert_eq!(
graph.get_chain_spend(
&local_chain,
tip.block_id(),
OutPoint::new(tx_0.compute_txid(), 1)
),
Some((ChainPosition::Unconfirmed(0), tx_2.compute_txid())),
);
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
let _ = graph.insert_seen_at(tx_2.compute_txid(), 1234567);
@@ -1099,10 +1089,10 @@ fn update_last_seen_unconfirmed() {
let txid = tx.compute_txid();
// insert a new tx
// initially we have a last_seen of 0, and no anchors
// initially we have a last_seen of None and no anchors
let _ = graph.insert_tx(tx);
let tx = graph.full_txs().next().unwrap();
assert_eq!(tx.last_seen_unconfirmed, 0);
assert_eq!(tx.last_seen_unconfirmed, None);
assert!(tx.anchors.is_empty());
// higher timestamp should update last seen
@@ -1117,7 +1107,56 @@ fn update_last_seen_unconfirmed() {
let _ = graph.insert_anchor(txid, ());
let changeset = graph.update_last_seen_unconfirmed(4);
assert!(changeset.is_empty());
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
assert_eq!(
graph
.full_txs()
.next()
.unwrap()
.last_seen_unconfirmed
.unwrap(),
2
);
}
#[test]
fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() {
let txs = vec![new_tx(0), new_tx(1)];
let txids: Vec<Txid> = txs.iter().map(Transaction::compute_txid).collect();
// graph
let mut graph = TxGraph::<BlockId>::new(txs);
let full_txs: Vec<_> = graph.full_txs().collect();
assert_eq!(full_txs.len(), 2);
let unseen_txs: Vec<_> = graph.txs_with_no_anchor_or_last_seen().collect();
assert_eq!(unseen_txs.len(), 2);
// chain
let blocks: BTreeMap<u32, BlockHash> = [(0, h!("g")), (1, h!("A")), (2, h!("B"))]
.into_iter()
.collect();
let chain = LocalChain::from_blocks(blocks).unwrap();
let canonical_txs: Vec<_> = graph
.list_canonical_txs(&chain, chain.tip().block_id())
.collect();
assert!(canonical_txs.is_empty());
// tx0 with seen_at should be returned by canonical txs
let _ = graph.insert_seen_at(txids[0], 2);
let mut canonical_txs = graph.list_canonical_txs(&chain, chain.tip().block_id());
assert_eq!(
canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(),
txids[0]
);
drop(canonical_txs);
// tx1 with anchor is also canonical
let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
let canonical_txids: Vec<_> = graph
.list_canonical_txs(&chain, chain.tip().block_id())
.map(|tx| tx.tx_node.txid)
.collect();
assert!(canonical_txids.contains(&txids[1]));
assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none());
}
#[test]

View File

@@ -15,7 +15,7 @@ struct Scenario<'a> {
name: &'a str,
/// Transaction templates
tx_templates: &'a [TxTemplate<'a, BlockId>],
/// Names of txs that must exist in the output of `list_chain_txs`
/// Names of txs that must exist in the output of `list_canonical_txs`
exp_chain_txs: HashSet<&'a str>,
/// Outpoints that must exist in the output of `filter_chain_txouts`
exp_chain_txouts: HashSet<(&'a str, u32)>,
@@ -27,7 +27,7 @@ struct Scenario<'a> {
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`],
/// This test also checks that [`TxGraph::list_canonical_txs`], [`TxGraph::filter_chain_txouts`],
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
#[test]
fn test_tx_conflict_handling() {
@@ -597,7 +597,7 @@ fn test_tx_conflict_handling() {
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
let txs = tx_graph
.list_chain_txs(&local_chain, chain_tip)
.list_canonical_txs(&local_chain, chain_tip)
.map(|tx| tx.tx_node.txid)
.collect::<BTreeSet<_>>();
let exp_txs = scenario
@@ -607,7 +607,7 @@ fn test_tx_conflict_handling() {
.collect::<BTreeSet<_>>();
assert_eq!(
txs, exp_txs,
"\n[{}] 'list_chain_txs' failed",
"\n[{}] 'list_canonical_txs' failed",
scenario.name
);