Skip to content

Commit

Permalink
Merge bitcoindevkit#1733: feat(chain,wallet)!: Transitive `ChainPosit…
Browse files Browse the repository at this point in the history
…ion`

29b374e feat(chain,wallet)!: Transitive `ChainPosition` (志宇)

Pull request description:

  ### Description

  Change `ChainPosition` to be able to represent transitive anchors and unconfirm-without-last-seen values.

  As mentioned in bitcoindevkit#1670 (comment), we want this merged first so that we have minimal changes to the API after 1670 is merged.

  ### Changelog notice

  * Change `ChainPosition` so that it is able to represent transitive anchors and unconfirmed-without-last-seen values.

  ### 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

  #### New Features:

  ~* [ ] I've added tests for the new feature~
  * [x] I've added docs for the new feature

ACKs for top commit:
  ValuedMammal:
    ACK 29b374e

Tree-SHA512: 58f22f38201304611341835f22f2526254009077cde6dfcd1f6051aec906ddae78f45ebd7900c35c0fb5165ed10e48a45a55fca73395edcf9bec2fb1daa1acc6
  • Loading branch information
ValuedMammal committed Dec 1, 2024
2 parents 00c33c4 + 29b374e commit f68b73c
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 120 deletions.
102 changes: 69 additions & 33 deletions crates/chain/src/chain_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,45 @@ use crate::Anchor;
))
)]
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 { .. })
}
}

impl<A: Clone> ChainPosition<&A> {
/// 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 },
}
}
}
Expand All @@ -42,8 +62,10 @@ impl<A: Anchor> ChainPosition<A> {
/// 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,
}
}
}
Expand Down Expand Up @@ -73,14 +95,14 @@ impl<A: Anchor> FullTxOut<A> {
/// [`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;
}
Expand All @@ -103,17 +125,21 @@ impl<A: Anchor> FullTxOut<A> {
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;
}
}
Expand All @@ -132,22 +158,32 @@ mod test {

#[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");
Expand Down
15 changes: 11 additions & 4 deletions crates/chain/src/tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,7 +770,12 @@ impl<A: Anchor> TxGraph<A> {

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,
}
}
Expand Down Expand Up @@ -877,7 +882,9 @@ impl<A: Anchor> TxGraph<A> {
}
}

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`.
Expand Down Expand Up @@ -1146,14 +1153,14 @@ impl<A: Anchor> TxGraph<A> {
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 {
Expand Down
17 changes: 10 additions & 7 deletions crates/chain/tests/test_indexed_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ fn test_list_owned_txouts() {
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
Expand All @@ -304,7 +304,7 @@ fn test_list_owned_txouts() {
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
Expand All @@ -315,7 +315,7 @@ fn test_list_owned_txouts() {
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
Expand All @@ -326,7 +326,7 @@ fn test_list_owned_txouts() {
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
Expand Down Expand Up @@ -618,7 +618,7 @@ fn test_get_chain_position() {
},
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",
Expand All @@ -631,7 +631,10 @@ fn test_get_chain_position() {
},
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",
Expand All @@ -644,7 +647,7 @@ fn test_get_chain_position() {
},
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",
Expand Down
45 changes: 30 additions & 15 deletions crates/chain/tests/test_tx_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -885,13 +885,16 @@ fn test_chain_spends() {
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(),
)),
);
Expand All @@ -900,13 +903,16 @@ fn test_chain_spends() {
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.
Expand All @@ -921,7 +927,12 @@ fn test_chain_spends() {
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.
Expand Down Expand Up @@ -957,7 +968,9 @@ fn test_chain_spends() {
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.
Expand All @@ -970,7 +983,9 @@ fn test_chain_spends() {
)
.expect("expect observation"),
(
ChainPosition::Unconfirmed(1234568),
ChainPosition::Unconfirmed {
last_seen: Some(1234568)
},
tx_2_conflict.compute_txid()
)
);
Expand Down
21 changes: 13 additions & 8 deletions crates/wallet/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,15 @@ pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoi
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)
}
Expand Down Expand Up @@ -270,11 +273,13 @@ pub fn receive_output_to_address(
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);
}
}
}

Expand Down
Loading

0 comments on commit f68b73c

Please sign in to comment.