Skip to content

Commit f68b73c

Browse files
committed
Merge #1733: feat(chain,wallet)!: Transitive ChainPosition
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 #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
2 parents 00c33c4 + 29b374e commit f68b73c

File tree

10 files changed

+239
-120
lines changed

10 files changed

+239
-120
lines changed

crates/chain/src/chain_data.rs

Lines changed: 69 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,45 @@ use crate::Anchor;
1515
))
1616
)]
1717
pub enum ChainPosition<A> {
18-
/// The chain data is seen as confirmed, and in anchored by `A`.
19-
Confirmed(A),
20-
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
21-
Unconfirmed(u64),
18+
/// The chain data is confirmed as it is anchored in the best chain by `A`.
19+
Confirmed {
20+
/// The [`Anchor`].
21+
anchor: A,
22+
/// Whether the chain data is anchored transitively by a child transaction.
23+
///
24+
/// If the value is `Some`, it means we have incomplete data. We can only deduce that the
25+
/// chain data is confirmed at a block equal to or lower than the block referenced by `A`.
26+
transitively: Option<Txid>,
27+
},
28+
/// The chain data is not confirmed.
29+
Unconfirmed {
30+
/// When the chain data is last seen in the mempool.
31+
///
32+
/// This value will be `None` if the chain data was never seen in the mempool and only seen
33+
/// in a conflicting chain.
34+
last_seen: Option<u64>,
35+
},
2236
}
2337

2438
impl<A> ChainPosition<A> {
2539
/// Returns whether [`ChainPosition`] is confirmed or not.
2640
pub fn is_confirmed(&self) -> bool {
27-
matches!(self, Self::Confirmed(_))
41+
matches!(self, Self::Confirmed { .. })
2842
}
2943
}
3044

3145
impl<A: Clone> ChainPosition<&A> {
3246
/// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
3347
pub fn cloned(self) -> ChainPosition<A> {
3448
match self {
35-
ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
36-
ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
49+
ChainPosition::Confirmed {
50+
anchor,
51+
transitively,
52+
} => ChainPosition::Confirmed {
53+
anchor: anchor.clone(),
54+
transitively,
55+
},
56+
ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen },
3757
}
3858
}
3959
}
@@ -42,8 +62,10 @@ impl<A: Anchor> ChainPosition<A> {
4262
/// Determines the upper bound of the confirmation height.
4363
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
4464
match self {
45-
ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
46-
ChainPosition::Unconfirmed(_) => None,
65+
ChainPosition::Confirmed { anchor, .. } => {
66+
Some(anchor.confirmation_height_upper_bound())
67+
}
68+
ChainPosition::Unconfirmed { .. } => None,
4769
}
4870
}
4971
}
@@ -73,14 +95,14 @@ impl<A: Anchor> FullTxOut<A> {
7395
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
7496
pub fn is_mature(&self, tip: u32) -> bool {
7597
if self.is_on_coinbase {
76-
let tx_height = match &self.chain_position {
77-
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
78-
ChainPosition::Unconfirmed(_) => {
98+
let conf_height = match self.chain_position.confirmation_height_upper_bound() {
99+
Some(height) => height,
100+
None => {
79101
debug_assert!(false, "coinbase tx can never be unconfirmed");
80102
return false;
81103
}
82104
};
83-
let age = tip.saturating_sub(tx_height);
105+
let age = tip.saturating_sub(conf_height);
84106
if age + 1 < COINBASE_MATURITY {
85107
return false;
86108
}
@@ -103,17 +125,21 @@ impl<A: Anchor> FullTxOut<A> {
103125
return false;
104126
}
105127

106-
let confirmation_height = match &self.chain_position {
107-
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
108-
ChainPosition::Unconfirmed(_) => return false,
128+
let conf_height = match self.chain_position.confirmation_height_upper_bound() {
129+
Some(height) => height,
130+
None => return false,
109131
};
110-
if confirmation_height > tip {
132+
if conf_height > tip {
111133
return false;
112134
}
113135

114136
// if the spending tx is confirmed within tip height, the txout is no longer spendable
115-
if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
116-
if spending_anchor.anchor_block().height <= tip {
137+
if let Some(spend_height) = self
138+
.spent_by
139+
.as_ref()
140+
.and_then(|(pos, _)| pos.confirmation_height_upper_bound())
141+
{
142+
if spend_height <= tip {
117143
return false;
118144
}
119145
}
@@ -132,22 +158,32 @@ mod test {
132158

133159
#[test]
134160
fn chain_position_ord() {
135-
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
136-
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
137-
let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
138-
confirmation_time: 20,
139-
block_id: BlockId {
140-
height: 9,
141-
..Default::default()
161+
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
162+
last_seen: Some(10),
163+
};
164+
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
165+
last_seen: Some(20),
166+
};
167+
let conf1 = ChainPosition::Confirmed {
168+
anchor: ConfirmationBlockTime {
169+
confirmation_time: 20,
170+
block_id: BlockId {
171+
height: 9,
172+
..Default::default()
173+
},
142174
},
143-
});
144-
let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
145-
confirmation_time: 15,
146-
block_id: BlockId {
147-
height: 12,
148-
..Default::default()
175+
transitively: None,
176+
};
177+
let conf2 = ChainPosition::Confirmed {
178+
anchor: ConfirmationBlockTime {
179+
confirmation_time: 15,
180+
block_id: BlockId {
181+
height: 12,
182+
..Default::default()
183+
},
149184
},
150-
});
185+
transitively: None,
186+
};
151187

152188
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
153189
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");

crates/chain/src/tx_graph.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,12 @@ impl<A: Anchor> TxGraph<A> {
770770

771771
for anchor in anchors {
772772
match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
773-
Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))),
773+
Some(true) => {
774+
return Ok(Some(ChainPosition::Confirmed {
775+
anchor,
776+
transitively: None,
777+
}))
778+
}
774779
_ => continue,
775780
}
776781
}
@@ -877,7 +882,9 @@ impl<A: Anchor> TxGraph<A> {
877882
}
878883
}
879884

880-
Ok(Some(ChainPosition::Unconfirmed(last_seen)))
885+
Ok(Some(ChainPosition::Unconfirmed {
886+
last_seen: Some(last_seen),
887+
}))
881888
}
882889

883890
/// Get the position of the transaction in `chain` with tip `chain_tip`.
@@ -1146,14 +1153,14 @@ impl<A: Anchor> TxGraph<A> {
11461153
let (spk_i, txout) = res?;
11471154

11481155
match &txout.chain_position {
1149-
ChainPosition::Confirmed(_) => {
1156+
ChainPosition::Confirmed { .. } => {
11501157
if txout.is_confirmed_and_spendable(chain_tip.height) {
11511158
confirmed += txout.txout.value;
11521159
} else if !txout.is_mature(chain_tip.height) {
11531160
immature += txout.txout.value;
11541161
}
11551162
}
1156-
ChainPosition::Unconfirmed(_) => {
1163+
ChainPosition::Unconfirmed { .. } => {
11571164
if trust_predicate(&spk_i, txout.txout.script_pubkey) {
11581165
trusted_pending += txout.txout.value;
11591166
} else {

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ fn test_list_owned_txouts() {
293293
let confirmed_txouts_txid = txouts
294294
.iter()
295295
.filter_map(|(_, full_txout)| {
296-
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
296+
if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
297297
Some(full_txout.outpoint.txid)
298298
} else {
299299
None
@@ -304,7 +304,7 @@ fn test_list_owned_txouts() {
304304
let unconfirmed_txouts_txid = txouts
305305
.iter()
306306
.filter_map(|(_, full_txout)| {
307-
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
307+
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
308308
Some(full_txout.outpoint.txid)
309309
} else {
310310
None
@@ -315,7 +315,7 @@ fn test_list_owned_txouts() {
315315
let confirmed_utxos_txid = utxos
316316
.iter()
317317
.filter_map(|(_, full_txout)| {
318-
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
318+
if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
319319
Some(full_txout.outpoint.txid)
320320
} else {
321321
None
@@ -326,7 +326,7 @@ fn test_list_owned_txouts() {
326326
let unconfirmed_utxos_txid = utxos
327327
.iter()
328328
.filter_map(|(_, full_txout)| {
329-
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
329+
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
330330
Some(full_txout.outpoint.txid)
331331
} else {
332332
None
@@ -618,7 +618,7 @@ fn test_get_chain_position() {
618618
},
619619
anchor: None,
620620
last_seen: Some(2),
621-
exp_pos: Some(ChainPosition::Unconfirmed(2)),
621+
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
622622
},
623623
TestCase {
624624
name: "tx anchor in best chain - confirmed",
@@ -631,7 +631,10 @@ fn test_get_chain_position() {
631631
},
632632
anchor: Some(blocks[1]),
633633
last_seen: None,
634-
exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
634+
exp_pos: Some(ChainPosition::Confirmed {
635+
anchor: blocks[1],
636+
transitively: None,
637+
}),
635638
},
636639
TestCase {
637640
name: "tx unknown anchor with last_seen - unconfirmed",
@@ -644,7 +647,7 @@ fn test_get_chain_position() {
644647
},
645648
anchor: Some(block_id!(2, "B'")),
646649
last_seen: Some(2),
647-
exp_pos: Some(ChainPosition::Unconfirmed(2)),
650+
exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
648651
},
649652
TestCase {
650653
name: "tx unknown anchor - no chain pos",

crates/chain/tests/test_tx_graph.rs

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -885,13 +885,16 @@ fn test_chain_spends() {
885885
OutPoint::new(tx_0.compute_txid(), 0)
886886
),
887887
Some((
888-
ChainPosition::Confirmed(&ConfirmationBlockTime {
889-
block_id: BlockId {
890-
hash: tip.get(98).unwrap().hash(),
891-
height: 98,
888+
ChainPosition::Confirmed {
889+
anchor: &ConfirmationBlockTime {
890+
block_id: BlockId {
891+
hash: tip.get(98).unwrap().hash(),
892+
height: 98,
893+
},
894+
confirmation_time: 100
892895
},
893-
confirmation_time: 100
894-
}),
896+
transitively: None
897+
},
895898
tx_1.compute_txid(),
896899
)),
897900
);
@@ -900,13 +903,16 @@ fn test_chain_spends() {
900903
assert_eq!(
901904
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
902905
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
903-
Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
904-
block_id: BlockId {
905-
hash: tip.get(95).unwrap().hash(),
906-
height: 95,
906+
Some(ChainPosition::Confirmed {
907+
anchor: &ConfirmationBlockTime {
908+
block_id: BlockId {
909+
hash: tip.get(95).unwrap().hash(),
910+
height: 95,
911+
},
912+
confirmation_time: 100
907913
},
908-
confirmation_time: 100
909-
}))
914+
transitively: None
915+
})
910916
);
911917

912918
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
@@ -921,7 +927,12 @@ fn test_chain_spends() {
921927
OutPoint::new(tx_0.compute_txid(), 1)
922928
)
923929
.unwrap(),
924-
(ChainPosition::Unconfirmed(1234567), tx_2.compute_txid())
930+
(
931+
ChainPosition::Unconfirmed {
932+
last_seen: Some(1234567)
933+
},
934+
tx_2.compute_txid()
935+
)
925936
);
926937

927938
// A conflicting transaction that conflicts with tx_1.
@@ -957,7 +968,9 @@ fn test_chain_spends() {
957968
graph
958969
.get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid())
959970
.expect("position expected"),
960-
ChainPosition::Unconfirmed(1234568)
971+
ChainPosition::Unconfirmed {
972+
last_seen: Some(1234568)
973+
}
961974
);
962975

963976
// Chain_spend now catches the new transaction as the spend.
@@ -970,7 +983,9 @@ fn test_chain_spends() {
970983
)
971984
.expect("expect observation"),
972985
(
973-
ChainPosition::Unconfirmed(1234568),
986+
ChainPosition::Unconfirmed {
987+
last_seen: Some(1234568)
988+
},
974989
tx_2_conflict.compute_txid()
975990
)
976991
);

crates/wallet/src/test_utils.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,15 @@ pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoi
229229
let latest_cp = wallet.latest_checkpoint();
230230
let height = latest_cp.height();
231231
let anchor = if height == 0 {
232-
ChainPosition::Unconfirmed(0)
232+
ChainPosition::Unconfirmed { last_seen: Some(0) }
233233
} else {
234-
ChainPosition::Confirmed(ConfirmationBlockTime {
235-
block_id: latest_cp.block_id(),
236-
confirmation_time: 0,
237-
})
234+
ChainPosition::Confirmed {
235+
anchor: ConfirmationBlockTime {
236+
block_id: latest_cp.block_id(),
237+
confirmation_time: 0,
238+
},
239+
transitively: None,
240+
}
238241
};
239242
receive_output(wallet, value, anchor)
240243
}
@@ -270,11 +273,13 @@ pub fn receive_output_to_address(
270273
insert_tx(wallet, tx);
271274

272275
match pos {
273-
ChainPosition::Confirmed(anchor) => {
276+
ChainPosition::Confirmed { anchor, .. } => {
274277
insert_anchor(wallet, txid, anchor);
275278
}
276-
ChainPosition::Unconfirmed(last_seen) => {
277-
insert_seen_at(wallet, txid, last_seen);
279+
ChainPosition::Unconfirmed { last_seen } => {
280+
if let Some(last_seen) = last_seen {
281+
insert_seen_at(wallet, txid, last_seen);
282+
}
278283
}
279284
}
280285

0 commit comments

Comments
 (0)