get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
- trusted_pending: SEND_AMOUNT * reorg_count as u64,
..Balance::default()
},
"reorg_count: {}",
#[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)>,
+ txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, Option<u64>)>,
spends: BTreeMap<OutPoint, HashSet<Txid>>,
anchors: BTreeSet<(A, Txid)>,
/// 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> {
(
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
BTreeSet::new(),
- 0,
+ None,
),
);
self.apply_update(update)
let mut update = Self::default();
update.txs.insert(
tx.compute_txid(),
- (TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
+ (TxNodeInternal::Whole(tx), BTreeSet::new(), None),
);
self.apply_update(update)
}
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 = Some(seen_at);
self.apply_update(update)
}
None => {
self.txs.insert(
txid,
- (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
+ (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), None),
);
}
}
for (txid, new_last_seen) in changeset.last_seen {
let (_, _, last_seen) = self.txs.entry(txid).or_default();
- if new_last_seen > *last_seen {
- *last_seen = new_last_seen;
+ if Some(new_last_seen) > *last_seen {
+ *last_seen = Some(new_last_seen);
}
}
}
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) {
+ let prev_last_seen = match (self.txs.get(&txid), update_tx_node) {
(None, TxNodeInternal::Whole(update_tx)) => {
changeset.txs.insert(update_tx.clone());
- 0
+ None
}
(None, TxNodeInternal::Partial(update_txos)) => {
changeset.txouts.extend(
.iter()
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
);
- 0
+ None
}
(Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen,
(
};
if *update_last_seen > prev_last_seen {
- changeset.last_seen.insert(txid, *update_last_seen);
+ changeset.last_seen.insert(
+ txid,
+ update_last_seen.expect("checked is greater, so we must have a last_seen"),
+ );
}
}
}
}
+ // 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 last_seen {
+ 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!
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
}
}
- Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
+ Ok(Some(ChainPosition::Unconfirmed(last_seen)))
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
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)
}
/// 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`.
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
..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),
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)| {
|_, 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)| {
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
}
);
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
}
);
}
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()
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!(
}))
);
- // 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);
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
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]
.apply_update(update.chain_update)
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
- // Check to see if a new anchor is added during current reorg.
- if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
- println!("New anchor added at reorg depth {}", depth);
- }
+ // Check that no new anchors are added during current reorg.
+ assert!(initial_anchors.is_superset(update.graph_update.all_anchors()));
let _ = recv_graph.apply_update(update.graph_update);
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
- trusted_pending: SEND_AMOUNT * depth as u64,
..Balance::default()
},
"reorg_count: {}",
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
- wallet.insert_tx(small_output_tx.clone());
+ 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()
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 =
version: transaction::Version::non_standard(0),
lock_time: absolute::LockTime::ZERO,
};
-
+ 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()
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();
value: Amount::from_sat(50_000),
}],
};
+ let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id;
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
}
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::BROADCAST_MIN);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(10));
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::ZERO);
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
let original_fee = check_fee!(wallet, psbt);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb
let mut builder = wallet.build_fee_bump(txid).unwrap();
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(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
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
assert_eq!(original_sent_received.0, Amount::from_sat(25_000));
let mut builder = wallet.build_fee_bump(txid).unwrap();
let original_details = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(50));
let original_sent_received = wallet.sent_and_received(&tx);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(Amount::from_sat(6_000));
let tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
// Now bump the fees, the wallet should add an extra input and a change output, and leave
// the original output untouched.
assert_eq!(tx.output.len(), 2);
let txid = tx.compute_txid();
wallet.insert_tx(tx);
+ wallet.insert_seen_at(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
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet.insert_tx(tx.clone());
+ wallet.insert_seen_at(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();
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet.insert_tx(tx.clone());
+ wallet.insert_seen_at(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()`
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb_unchecked(25));
builder.finish().unwrap();
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet.insert_tx(tx);
+ wallet.insert_seen_at(txid, 0);
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder