))
)]
pub enum ChainPosition<A> {
- /// The chain data is seen as confirmed, and in anchored by `A`.
- Confirmed(A),
- /// The chain data is not confirmed and last seen in the mempool at this timestamp.
- Unconfirmed(u64),
+ /// The chain data is confirmed as it is anchored in the best chain by `A`.
+ Confirmed {
+ /// The [`Anchor`].
+ anchor: A,
+ /// Whether the chain data is anchored transitively by a child transaction.
+ ///
+ /// If the value is `Some`, it means we have incomplete data. We can only deduce that the
+ /// chain data is confirmed at a block equal to or lower than the block referenced by `A`.
+ transitively: Option<Txid>,
+ },
+ /// The chain data is not confirmed.
+ Unconfirmed {
+ /// When the chain data is last seen in the mempool.
+ ///
+ /// This value will be `None` if the chain data was never seen in the mempool and only seen
+ /// in a conflicting chain.
+ last_seen: Option<u64>,
+ },
}
impl<A> ChainPosition<A> {
/// Returns whether [`ChainPosition`] is confirmed or not.
pub fn is_confirmed(&self) -> bool {
- matches!(self, Self::Confirmed(_))
+ matches!(self, Self::Confirmed { .. })
}
}
/// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
pub fn cloned(self) -> ChainPosition<A> {
match self {
- ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
- ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
+ ChainPosition::Confirmed {
+ anchor,
+ transitively,
+ } => ChainPosition::Confirmed {
+ anchor: anchor.clone(),
+ transitively,
+ },
+ ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen },
}
}
}
/// Determines the upper bound of the confirmation height.
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
match self {
- ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
- ChainPosition::Unconfirmed(_) => None,
+ ChainPosition::Confirmed { anchor, .. } => {
+ Some(anchor.confirmation_height_upper_bound())
+ }
+ ChainPosition::Unconfirmed { .. } => None,
}
}
}
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
pub fn is_mature(&self, tip: u32) -> bool {
if self.is_on_coinbase {
- let tx_height = match &self.chain_position {
- ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
- ChainPosition::Unconfirmed(_) => {
+ let conf_height = match self.chain_position.confirmation_height_upper_bound() {
+ Some(height) => height,
+ None => {
debug_assert!(false, "coinbase tx can never be unconfirmed");
return false;
}
};
- let age = tip.saturating_sub(tx_height);
+ let age = tip.saturating_sub(conf_height);
if age + 1 < COINBASE_MATURITY {
return false;
}
return false;
}
- let confirmation_height = match &self.chain_position {
- ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
- ChainPosition::Unconfirmed(_) => return false,
+ let conf_height = match self.chain_position.confirmation_height_upper_bound() {
+ Some(height) => height,
+ None => return false,
};
- if confirmation_height > tip {
+ if conf_height > tip {
return false;
}
// if the spending tx is confirmed within tip height, the txout is no longer spendable
- if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
- if spending_anchor.anchor_block().height <= tip {
+ if let Some(spend_height) = self
+ .spent_by
+ .as_ref()
+ .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
+ {
+ if spend_height <= tip {
return false;
}
}
#[test]
fn chain_position_ord() {
- let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
- let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
- let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
- confirmation_time: 20,
- block_id: BlockId {
- height: 9,
- ..Default::default()
+ let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
+ last_seen: Some(10),
+ };
+ let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
+ last_seen: Some(20),
+ };
+ let conf1 = ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ confirmation_time: 20,
+ block_id: BlockId {
+ height: 9,
+ ..Default::default()
+ },
},
- });
- let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
- confirmation_time: 15,
- block_id: BlockId {
- height: 12,
- ..Default::default()
+ transitively: None,
+ };
+ let conf2 = ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ confirmation_time: 15,
+ block_id: BlockId {
+ height: 12,
+ ..Default::default()
+ },
},
- });
+ transitively: None,
+ };
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
for anchor in anchors {
match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
- Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))),
+ Some(true) => {
+ return Ok(Some(ChainPosition::Confirmed {
+ anchor,
+ transitively: None,
+ }))
+ }
_ => continue,
}
}
}
}
- Ok(Some(ChainPosition::Unconfirmed(last_seen)))
+ Ok(Some(ChainPosition::Unconfirmed {
+ last_seen: Some(last_seen),
+ }))
}
/// Get the position of the transaction in `chain` with tip `chain_tip`.
let (spk_i, txout) = res?;
match &txout.chain_position {
- ChainPosition::Confirmed(_) => {
+ ChainPosition::Confirmed { .. } => {
if txout.is_confirmed_and_spendable(chain_tip.height) {
confirmed += txout.txout.value;
} else if !txout.is_mature(chain_tip.height) {
immature += txout.txout.value;
}
}
- ChainPosition::Unconfirmed(_) => {
+ ChainPosition::Unconfirmed { .. } => {
if trust_predicate(&spk_i, txout.txout.script_pubkey) {
trusted_pending += txout.txout.value;
} else {
let confirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
let unconfirmed_txouts_txid = txouts
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
let confirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
let unconfirmed_utxos_txid = utxos
.iter()
.filter_map(|(_, full_txout)| {
- if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
+ if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
Some(full_txout.outpoint.txid)
} else {
None
},
anchor: None,
last_seen: Some(2),
- exp_pos: Some(ChainPosition::Unconfirmed(2)),
+ exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
},
TestCase {
name: "tx anchor in best chain - confirmed",
},
anchor: Some(blocks[1]),
last_seen: None,
- exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
+ exp_pos: Some(ChainPosition::Confirmed {
+ anchor: blocks[1],
+ transitively: None,
+ }),
},
TestCase {
name: "tx unknown anchor with last_seen - unconfirmed",
},
anchor: Some(block_id!(2, "B'")),
last_seen: Some(2),
- exp_pos: Some(ChainPosition::Unconfirmed(2)),
+ exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
},
TestCase {
name: "tx unknown anchor - no chain pos",
OutPoint::new(tx_0.compute_txid(), 0)
),
Some((
- ChainPosition::Confirmed(&ConfirmationBlockTime {
- block_id: BlockId {
- hash: tip.get(98).unwrap().hash(),
- height: 98,
+ ChainPosition::Confirmed {
+ anchor: &ConfirmationBlockTime {
+ block_id: BlockId {
+ hash: tip.get(98).unwrap().hash(),
+ height: 98,
+ },
+ confirmation_time: 100
},
- confirmation_time: 100
- }),
+ transitively: None
+ },
tx_1.compute_txid(),
)),
);
assert_eq!(
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
- Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
- block_id: BlockId {
- hash: tip.get(95).unwrap().hash(),
- height: 95,
+ Some(ChainPosition::Confirmed {
+ anchor: &ConfirmationBlockTime {
+ block_id: BlockId {
+ hash: tip.get(95).unwrap().hash(),
+ height: 95,
+ },
+ confirmation_time: 100
},
- confirmation_time: 100
- }))
+ transitively: None
+ })
);
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
OutPoint::new(tx_0.compute_txid(), 1)
)
.unwrap(),
- (ChainPosition::Unconfirmed(1234567), tx_2.compute_txid())
+ (
+ ChainPosition::Unconfirmed {
+ last_seen: Some(1234567)
+ },
+ tx_2.compute_txid()
+ )
);
// A conflicting transaction that conflicts with tx_1.
graph
.get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid())
.expect("position expected"),
- ChainPosition::Unconfirmed(1234568)
+ ChainPosition::Unconfirmed {
+ last_seen: Some(1234568)
+ }
);
// Chain_spend now catches the new transaction as the spend.
)
.expect("expect observation"),
(
- ChainPosition::Unconfirmed(1234568),
+ ChainPosition::Unconfirmed {
+ last_seen: Some(1234568)
+ },
tx_2_conflict.compute_txid()
)
);
let latest_cp = wallet.latest_checkpoint();
let height = latest_cp.height();
let anchor = if height == 0 {
- ChainPosition::Unconfirmed(0)
+ ChainPosition::Unconfirmed { last_seen: Some(0) }
} else {
- ChainPosition::Confirmed(ConfirmationBlockTime {
- block_id: latest_cp.block_id(),
- confirmation_time: 0,
- })
+ ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ block_id: latest_cp.block_id(),
+ confirmation_time: 0,
+ },
+ transitively: None,
+ }
};
receive_output(wallet, value, anchor)
}
insert_tx(wallet, tx);
match pos {
- ChainPosition::Confirmed(anchor) => {
+ ChainPosition::Confirmed { anchor, .. } => {
insert_anchor(wallet, txid, anchor);
}
- ChainPosition::Unconfirmed(last_seen) => {
- insert_seen_at(wallet, txid, last_seen);
+ ChainPosition::Unconfirmed { last_seen } => {
+ if let Some(last_seen) = last_seen {
+ insert_seen_at(wallet, txid, last_seen);
+ }
}
}
const FEE_AMOUNT: u64 = 50;
fn unconfirmed_utxo(value: u64, index: u32, last_seen: u64) -> WeightedUtxo {
- utxo(value, index, ChainPosition::Unconfirmed(last_seen))
+ utxo(
+ value,
+ index,
+ ChainPosition::Unconfirmed {
+ last_seen: Some(last_seen),
+ },
+ )
}
fn confirmed_utxo(
utxo(
value,
index,
- ChainPosition::Confirmed(ConfirmationBlockTime {
- block_id: chain::BlockId {
- height: confirmation_height,
- hash: bitcoin::BlockHash::all_zeros(),
+ ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ block_id: chain::BlockId {
+ height: confirmation_height,
+ hash: bitcoin::BlockHash::all_zeros(),
+ },
+ confirmation_time,
},
- confirmation_time,
- }),
+ transitively: None,
+ },
)
}
is_spent: false,
derivation_index: rng.next_u32(),
chain_position: if rng.gen_bool(0.5) {
- ChainPosition::Confirmed(ConfirmationBlockTime {
- block_id: chain::BlockId {
- height: rng.next_u32(),
- hash: BlockHash::all_zeros(),
+ ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ block_id: chain::BlockId {
+ height: rng.next_u32(),
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: rng.next_u64(),
},
- confirmation_time: rng.next_u64(),
- })
+ transitively: None,
+ }
} else {
- ChainPosition::Unconfirmed(0)
+ ChainPosition::Unconfirmed { last_seen: Some(0) }
},
}),
});
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 42,
- chain_position: ChainPosition::Unconfirmed(0),
+ chain_position: ChainPosition::Unconfirmed { last_seen: Some(0) },
}),
})
.collect()
optional.push(utxo(
500_000,
3,
- ChainPosition::<ConfirmationBlockTime>::Unconfirmed(0),
+ ChainPosition::<ConfirmationBlockTime>::Unconfirmed { last_seen: Some(0) },
));
// Defensive assertions, for sanity and in case someone changes the test utxos vector.
keychain: KeychainKind::External,
is_spent: false,
derivation_index: 0,
- chain_position: ChainPosition::Confirmed(ConfirmationBlockTime {
- block_id: BlockId {
- height: 12345,
- hash: BlockHash::all_zeros(),
+ chain_position: ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ block_id: BlockId {
+ height: 12345,
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: 12345,
},
- confirmation_time: 12345,
- }),
+ transitively: None,
+ },
}),
}
}
let blockheight = if include_blockheight {
wallet.transactions().next().map_or(0, |canonical_tx| {
- match canonical_tx.chain_position {
- bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height,
- bdk_chain::ChainPosition::Unconfirmed(_) => 0,
- }
+ canonical_tx
+ .chain_position
+ .confirmation_height_upper_bound()
+ .unwrap_or(0)
})
} else {
0
///
/// // get confirmation status of transaction
/// match wallet_tx.chain_position {
- /// ChainPosition::Confirmed(anchor) => println!(
+ /// ChainPosition::Confirmed {
+ /// anchor,
+ /// transitively: None,
+ /// } => println!(
/// "tx is confirmed at height {}, we know this since {}:{} is in the best chain",
/// anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash,
/// ),
- /// ChainPosition::Unconfirmed(last_seen) => println!(
- /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain",
+ /// ChainPosition::Confirmed {
+ /// anchor,
+ /// transitively: Some(_),
+ /// } => println!(
+ /// "tx is an ancestor of a tx anchored in {}:{}",
+ /// anchor.block_id.height, anchor.block_id.hash,
+ /// ),
+ /// ChainPosition::Unconfirmed { last_seen } => println!(
+ /// "tx is last seen at {:?}, it is unconfirmed as it is not anchored in the best chain",
/// last_seen,
/// ),
/// }
let pos = graph
.get_chain_position(&self.chain, chain_tip, txid)
.ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
- if let ChainPosition::Confirmed(_) = pos {
+ if pos.is_confirmed() {
return Err(BuildFeeBumpError::TransactionConfirmed(txid));
}
.indexed_graph
.graph()
.get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
- .map(|chain_position| match chain_position {
- ChainPosition::Confirmed(a) => a.block_id.height,
- ChainPosition::Unconfirmed(_) => u32::MAX,
+ .map(|chain_position| {
+ chain_position
+ .confirmation_height_upper_bound()
+ .unwrap_or(u32::MAX)
});
let current_height = sign_options
.assume_height
);
if let Some(current_height) = current_height {
match chain_position {
- ChainPosition::Confirmed(a) => {
+ ChainPosition::Confirmed { anchor, .. } => {
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
- spendable &= (current_height.saturating_sub(a.block_id.height))
+ spendable &= (current_height
+ .saturating_sub(anchor.block_id.height))
>= COINBASE_MATURITY;
}
ChainPosition::Unconfirmed { .. } => spendable = false,
txout: TxOut::NULL,
keychain: KeychainKind::External,
is_spent: false,
- chain_position: chain::ChainPosition::Unconfirmed(0),
+ chain_position: chain::ChainPosition::Unconfirmed { last_seen: Some(0) },
derivation_index: 0,
},
LocalOutput {
txout: TxOut::NULL,
keychain: KeychainKind::Internal,
is_spent: false,
- chain_position: chain::ChainPosition::Confirmed(chain::ConfirmationBlockTime {
- block_id: chain::BlockId {
- height: 32,
- hash: bitcoin::BlockHash::all_zeros(),
+ chain_position: chain::ChainPosition::Confirmed {
+ anchor: chain::ConfirmationBlockTime {
+ block_id: chain::BlockId {
+ height: 32,
+ hash: bitcoin::BlockHash::all_zeros(),
+ },
+ confirmation_time: 42,
},
- confirmation_time: 42,
- }),
+ transitively: None,
+ },
derivation_index: 1,
},
]
.create_wallet_no_persist()
.unwrap();
// fund wallet
- receive_output(&mut wallet, amount, ChainPosition::Unconfirmed(0));
+ receive_output(
+ &mut wallet,
+ amount,
+ ChainPosition::Unconfirmed { last_seen: Some(0) },
+ );
// create tx
let mut builder = wallet.build_tx();
builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send));
let psbt = builder.finish().unwrap();
// Now we receive one transaction with 0 confirmations. We won't be able to use that for
// fee bumping, as it's still unconfirmed!
- receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
+ receive_output(
+ &mut wallet,
+ 25_000,
+ ChainPosition::Unconfirmed { last_seen: Some(0) },
+ );
let mut tx = psbt.extract_tx().expect("failed to extract tx");
let txid = tx.compute_txid();
for txin in &mut tx.input {
.assume_checked();
// We receive a tx with 0 confirmations, which will be used as an input
// in the drain tx.
- receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
+ receive_output(
+ &mut wallet,
+ 25_000,
+ ChainPosition::Unconfirmed { last_seen: Some(0) },
+ );
let mut builder = wallet.build_tx();
builder.drain_wallet().drain_to(addr.script_pubkey());
let psbt = builder.finish().unwrap();
.last()
.unwrap()
.address;
- let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
- block_id: BlockId {
- height: 2000,
- hash: BlockHash::all_zeros(),
+ let chain_position = ChainPosition::Confirmed {
+ anchor: ConfirmationBlockTime {
+ block_id: BlockId {
+ height: 2000,
+ hash: BlockHash::all_zeros(),
+ },
+ confirmation_time: 0,
},
- confirmation_time: 0,
- });
+ transitively: None,
+ };
let _outpoint = receive_output_to_address(&mut wallet, addr, 8000, chain_position);
assert_eq!(wallet.balance().confirmed, Amount::from_sat(58000));
}
.unwrap();
assert_eq!(wallet.keychains().count(), 1);
let amt = Amount::from_sat(5_000);
- receive_output(&mut wallet, 2 * amt.to_sat(), ChainPosition::Unconfirmed(2));
+ receive_output(
+ &mut wallet,
+ 2 * amt.to_sat(),
+ ChainPosition::Unconfirmed { last_seen: Some(2) },
+ );
// create spend tx that produces a change output
let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")
.unwrap()
#[test]
fn test_transactions_sort_by() {
let (mut wallet, _txid) = get_funded_wallet_wpkh();
- receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
+ receive_output(
+ &mut wallet,
+ 25_000,
+ ChainPosition::Unconfirmed { last_seen: Some(0) },
+ );
// sort by chain position, unconfirmed then confirmed by descending block height
let sorted_txs: Vec<WalletTx> =