Skip to content

Commit b26ff89

Browse files
committed
Merge #1838: Make full-scan/sync flow easier to reason about.
800f358 feat!: Improve spk-based syncing flow (志宇) ee52745 feat(core)!: Make `TxUpdate` non-exhaustive (志宇) Pull request description: ### Description #1811 introduces the `evicted-at` concept. While adding this to `TxUpdate`, I realized there were some shortcomings in our full-scan & sync flow: * Chain sources that use `TxUpdate` to convey updates cannot choose to add transactions without a `seen_at` timestamp as the `TxGraph::apply_update_at` logic adds this timestamp for all unanchored txs if the `seen_at` parameter is specified. Purposefully adding uncanonical txs is useful for wallets that want to know about replaced txs. * The `TxGraph::apply_update_at` logic is hard to reason about. `TxUpdate::seen_ats` already has the `seen_at` timestamp per tx, but `apply_update_at()` also takes in a `seen_at` timestamp`. * `TxUpdate::seen_ats` currently forces us to only have one `seen-at` per tx as it's a map of `txid -> seen_at`. However, in the future we want to add a `first-seen` timestamp to `TxGraph` which is basically the earliest `seen-at` timestamp introduced so we may want to have multiple `seen_at`s per tx. The other issue is if we merge `TxUpdate`s, we will loose data. I.e. we can only keep the `last_seen` or the `first_seen`. These problems were exacerbated when introducing `evicted-at`. In the old design, the chain-source has no concept of sync time (as sync time was introduced after-the-fact when applying the `TxUpdate`). Therefore the introduced `TxUpdate::evicted` field was a `HashSet<Txid>` (no timestamps) and relied on the `TxGraph::apply_upate_at` logic to introduce the `evicted-at` timestamp. All this makes the sync logic harder to understand. What happens if the `seen_at` input is `None`? What happens if updates were applied out of order? What happens when we merge `TxUpdates` before applying? The following changes are made in this PR to simplify the sync/full-scan logic to be easier to understand and robust: * The `sync_time` is added to the `SyncRequest` and `FullScanRequest`. `sync_time` is mandatory and is added as an input of `builder()`. If the `std` feature is enabled, the `builder_now()` is available which uses the current timestamp. The chain source is now responsible to add this `sync_time` timestamp as `seen_at` for mempool txs. Non-canonical and non-anchored txs can be added without the `seen_at` timestamp. * `TxUpdate::seen_ats` is now a `HashSet` of `(Txid, u64)`. This allows multiple `seen_at`s per tx. This is also a much easier to use API as the chain source can just insert into this `HashSet` without checking previous data. * `TxGraph::apply_update_at` is removed as we no longer needs to introduce a fallback `seen_at` timestamp after-the-fact. `TxGraph::apply_update` is no longer `std`-only and the logic of applying updates is greatly simplified. Additionally, `TxUpdate` is updated to be `#[non_exhaustive]` for backwards-compatibility protection. ### Notes to the reviewers This is based on #1837. Merge that first. These are breaking changes to `bdk_core`. It needs to be breaking to properly fix all the issues. As mentioned by @notmandatory, `bdk_wallet` changes will be added to a new PR once the new `bdk_wallet` repo is ready. But the PR won't be merged until we're ready for a `bdk_wallet` 2.0. ### Changelog notice * Change `FullScanRequest::builder` and `SyncRequest::builder` methods to depend on `feature = "std"`. This is because requests now have a `start_time`, instead of specifying a `seen_at` when applying the update. * Add `FullScanRequest::builder_at` and `SyncRequest::builder_at` methods which are the non-std version of the `..Request::builder` methods. * Change `TxUpdate::seen_ats` field to be a `HashSet` of `(Txid, u64)`. This allows a single update to have multiple `seen_at`s per tx. * Change `TxUpdate` to be `non-exhaustive`. * Remove `apply_update_at` as we no longer need to apply with a timestamp after-the-fact. ### 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~ No tests needed as it's more of a refactor. * [x] I've added docs for the new feature ACKs for top commit: notmandatory: utACK 800f358 Tree-SHA512: 85af8452ac60c4a8087962403fd10c5c67592d3711f7665ae09a2d9c48a868583a41e54f28d639e47bd264ccf95d9254efc8d0d6248c8bcc9343c8290502e061
2 parents 209570d + 800f358 commit b26ff89

File tree

11 files changed

+298
-228
lines changed

11 files changed

+298
-228
lines changed

crates/chain/benches/canonicalization.rs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,10 @@ pub fn many_conflicting_unconfirmed(c: &mut Criterion) {
132132
}],
133133
..new_tx(i)
134134
};
135-
let update = TxUpdate {
136-
txs: vec![Arc::new(tx)],
137-
..Default::default()
138-
};
139-
let _ = tx_graph.apply_update_at(update, Some(i as u64));
135+
let mut update = TxUpdate::default();
136+
update.seen_ats = [(tx.compute_txid(), i as u64)].into();
137+
update.txs = vec![Arc::new(tx)];
138+
let _ = tx_graph.apply_update(update);
140139
}
141140
}));
142141
c.bench_function("many_conflicting_unconfirmed::list_canonical_txs", {
@@ -169,11 +168,10 @@ pub fn many_chained_unconfirmed(c: &mut Criterion) {
169168
..new_tx(i)
170169
};
171170
let txid = tx.compute_txid();
172-
let update = TxUpdate {
173-
txs: vec![Arc::new(tx)],
174-
..Default::default()
175-
};
176-
let _ = tx_graph.apply_update_at(update, Some(i as u64));
171+
let mut update = TxUpdate::default();
172+
update.seen_ats = [(txid, i as u64)].into();
173+
update.txs = vec![Arc::new(tx)];
174+
let _ = tx_graph.apply_update(update);
177175
// Store the next prevout.
178176
previous_output = OutPoint::new(txid, 0);
179177
}

crates/chain/src/indexed_tx_graph.rs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -91,37 +91,12 @@ where
9191
/// Apply an `update` directly.
9292
///
9393
/// `update` is a [`tx_graph::TxUpdate<A>`] and the resultant changes is returned as [`ChangeSet`].
94-
#[cfg(feature = "std")]
95-
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
9694
pub fn apply_update(&mut self, update: tx_graph::TxUpdate<A>) -> ChangeSet<A, I::ChangeSet> {
9795
let tx_graph = self.graph.apply_update(update);
9896
let indexer = self.index_tx_graph_changeset(&tx_graph);
9997
ChangeSet { tx_graph, indexer }
10098
}
10199

102-
/// Apply the given `update` with an optional `seen_at` timestamp.
103-
///
104-
/// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the
105-
/// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The
106-
/// `last_seen` value is used internally to determine precedence of conflicting unconfirmed
107-
/// transactions (where the transaction with the lower `last_seen` value is omitted from the
108-
/// canonical history).
109-
///
110-
/// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will
111-
/// not be part of the canonical history of transactions.
112-
///
113-
/// Use [`apply_update`](IndexedTxGraph::apply_update) to have the `seen_at` value automatically
114-
/// set to the current time.
115-
pub fn apply_update_at(
116-
&mut self,
117-
update: tx_graph::TxUpdate<A>,
118-
seen_at: Option<u64>,
119-
) -> ChangeSet<A, I::ChangeSet> {
120-
let tx_graph = self.graph.apply_update_at(update, seen_at);
121-
let indexer = self.index_tx_graph_changeset(&tx_graph);
122-
ChangeSet { tx_graph, indexer }
123-
}
124-
125100
/// Insert a floating `txout` of given `outpoint`.
126101
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
127102
let graph = self.graph.insert_txout(outpoint, txout);

crates/chain/src/tx_graph.rs

Lines changed: 15 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -110,26 +110,26 @@ use core::{
110110

111111
impl<A: Ord> From<TxGraph<A>> for TxUpdate<A> {
112112
fn from(graph: TxGraph<A>) -> Self {
113-
Self {
114-
txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(),
115-
txouts: graph
116-
.floating_txouts()
117-
.map(|(op, txo)| (op, txo.clone()))
118-
.collect(),
119-
anchors: graph
120-
.anchors
121-
.into_iter()
122-
.flat_map(|(txid, anchors)| anchors.into_iter().map(move |a| (a, txid)))
123-
.collect(),
124-
seen_ats: graph.last_seen.into_iter().collect(),
125-
}
113+
let mut tx_update = TxUpdate::default();
114+
tx_update.txs = graph.full_txs().map(|tx_node| tx_node.tx).collect();
115+
tx_update.txouts = graph
116+
.floating_txouts()
117+
.map(|(op, txo)| (op, txo.clone()))
118+
.collect();
119+
tx_update.anchors = graph
120+
.anchors
121+
.into_iter()
122+
.flat_map(|(txid, anchors)| anchors.into_iter().map(move |a| (a, txid)))
123+
.collect();
124+
tx_update.seen_ats = graph.last_seen.into_iter().collect();
125+
tx_update
126126
}
127127
}
128128

129129
impl<A: Anchor> From<TxUpdate<A>> for TxGraph<A> {
130130
fn from(update: TxUpdate<A>) -> Self {
131131
let mut graph = TxGraph::<A>::default();
132-
let _ = graph.apply_update_at(update, None);
132+
let _ = graph.apply_update(update);
133133
graph
134134
}
135135
}
@@ -719,52 +719,20 @@ impl<A: Anchor> TxGraph<A> {
719719
///
720720
/// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that
721721
/// exist in `update` but not in `self`).
722-
#[cfg(feature = "std")]
723-
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
724722
pub fn apply_update(&mut self, update: TxUpdate<A>) -> ChangeSet<A> {
725-
use std::time::*;
726-
let now = SystemTime::now()
727-
.duration_since(UNIX_EPOCH)
728-
.expect("current time must be greater than epoch anchor");
729-
self.apply_update_at(update, Some(now.as_secs()))
730-
}
731-
732-
/// Extends this graph with the given `update` alongside an optional `seen_at` timestamp.
733-
///
734-
/// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the
735-
/// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The
736-
/// `last_seen` value is used internally to determine precedence of conflicting unconfirmed
737-
/// transactions (where the transaction with the lower `last_seen` value is omitted from the
738-
/// canonical history).
739-
///
740-
/// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will
741-
/// not be part of the canonical history of transactions.
742-
///
743-
/// Use [`apply_update`](TxGraph::apply_update) to have the `seen_at` value automatically set
744-
/// to the current time.
745-
pub fn apply_update_at(&mut self, update: TxUpdate<A>, seen_at: Option<u64>) -> ChangeSet<A> {
746723
let mut changeset = ChangeSet::<A>::default();
747-
let mut unanchored_txs = HashSet::<Txid>::new();
748724
for tx in update.txs {
749-
if unanchored_txs.insert(tx.compute_txid()) {
750-
changeset.merge(self.insert_tx(tx));
751-
}
725+
changeset.merge(self.insert_tx(tx));
752726
}
753727
for (outpoint, txout) in update.txouts {
754728
changeset.merge(self.insert_txout(outpoint, txout));
755729
}
756730
for (anchor, txid) in update.anchors {
757-
unanchored_txs.remove(&txid);
758731
changeset.merge(self.insert_anchor(txid, anchor));
759732
}
760733
for (txid, seen_at) in update.seen_ats {
761734
changeset.merge(self.insert_seen_at(txid, seen_at));
762735
}
763-
if let Some(seen_at) = seen_at {
764-
for txid in unanchored_txs {
765-
changeset.merge(self.insert_seen_at(txid, seen_at));
766-
}
767-
}
768736
changeset
769737
}
770738

crates/chain/tests/test_tx_graph.rs

Lines changed: 56 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ fn insert_txouts() {
9494
// Insert partials transactions.
9595
update.txouts.insert(*outpoint, txout.clone());
9696
// Mark them unconfirmed.
97-
update.seen_ats.insert(outpoint.txid, unconf_seen_at);
97+
update.seen_ats.insert((outpoint.txid, unconf_seen_at));
9898
}
9999

100100
// Insert the full transaction.
@@ -1231,74 +1231,65 @@ fn tx_graph_update_conversion() {
12311231

12321232
let test_cases: &[TestCase] = &[
12331233
("empty_update", TxUpdate::default()),
1234-
(
1235-
"single_tx",
1236-
TxUpdate {
1237-
txs: vec![make_tx(0).into()],
1238-
..Default::default()
1239-
},
1240-
),
1241-
(
1242-
"two_txs",
1243-
TxUpdate {
1244-
txs: vec![make_tx(0).into(), make_tx(1).into()],
1245-
..Default::default()
1246-
},
1247-
),
1248-
(
1249-
"with_floating_txouts",
1250-
TxUpdate {
1251-
txs: vec![make_tx(0).into(), make_tx(1).into()],
1252-
txouts: [
1253-
(OutPoint::new(hash!("a"), 0), make_txout(0)),
1254-
(OutPoint::new(hash!("a"), 1), make_txout(1)),
1255-
(OutPoint::new(hash!("b"), 0), make_txout(2)),
1256-
]
1257-
.into(),
1258-
..Default::default()
1259-
},
1260-
),
1261-
(
1262-
"with_anchors",
1263-
TxUpdate {
1264-
txs: vec![make_tx(0).into(), make_tx(1).into()],
1265-
txouts: [
1266-
(OutPoint::new(hash!("a"), 0), make_txout(0)),
1267-
(OutPoint::new(hash!("a"), 1), make_txout(1)),
1268-
(OutPoint::new(hash!("b"), 0), make_txout(2)),
1269-
]
1270-
.into(),
1271-
anchors: [
1272-
(ConfirmationBlockTime::default(), hash!("a")),
1273-
(ConfirmationBlockTime::default(), hash!("b")),
1274-
]
1275-
.into(),
1276-
..Default::default()
1277-
},
1278-
),
1279-
(
1280-
"with_seen_ats",
1281-
TxUpdate {
1282-
txs: vec![make_tx(0).into(), make_tx(1).into()],
1283-
txouts: [
1284-
(OutPoint::new(hash!("a"), 0), make_txout(0)),
1285-
(OutPoint::new(hash!("a"), 1), make_txout(1)),
1286-
(OutPoint::new(hash!("d"), 0), make_txout(2)),
1287-
]
1288-
.into(),
1289-
anchors: [
1290-
(ConfirmationBlockTime::default(), hash!("a")),
1291-
(ConfirmationBlockTime::default(), hash!("b")),
1292-
]
1293-
.into(),
1294-
seen_ats: [(hash!("c"), 12346)].into_iter().collect(),
1295-
},
1296-
),
1234+
("single_tx", {
1235+
let mut tx_update = TxUpdate::default();
1236+
tx_update.txs = vec![make_tx(0).into()];
1237+
tx_update
1238+
}),
1239+
("two_txs", {
1240+
let mut tx_update = TxUpdate::default();
1241+
tx_update.txs = vec![make_tx(0).into(), make_tx(1).into()];
1242+
tx_update
1243+
}),
1244+
("with_floating_txouts", {
1245+
let mut tx_update = TxUpdate::default();
1246+
tx_update.txs = vec![make_tx(0).into(), make_tx(1).into()];
1247+
tx_update.txouts = [
1248+
(OutPoint::new(hash!("a"), 0), make_txout(0)),
1249+
(OutPoint::new(hash!("a"), 1), make_txout(1)),
1250+
(OutPoint::new(hash!("b"), 0), make_txout(2)),
1251+
]
1252+
.into();
1253+
tx_update
1254+
}),
1255+
("with_anchors", {
1256+
let mut tx_update = TxUpdate::default();
1257+
tx_update.txs = vec![make_tx(0).into(), make_tx(1).into()];
1258+
tx_update.txouts = [
1259+
(OutPoint::new(hash!("a"), 0), make_txout(0)),
1260+
(OutPoint::new(hash!("a"), 1), make_txout(1)),
1261+
(OutPoint::new(hash!("b"), 0), make_txout(2)),
1262+
]
1263+
.into();
1264+
tx_update.anchors = [
1265+
(ConfirmationBlockTime::default(), hash!("a")),
1266+
(ConfirmationBlockTime::default(), hash!("b")),
1267+
]
1268+
.into();
1269+
tx_update
1270+
}),
1271+
("with_seen_ats", {
1272+
let mut tx_update = TxUpdate::default();
1273+
tx_update.txs = vec![make_tx(0).into(), make_tx(1).into()];
1274+
tx_update.txouts = [
1275+
(OutPoint::new(hash!("a"), 0), make_txout(0)),
1276+
(OutPoint::new(hash!("a"), 1), make_txout(1)),
1277+
(OutPoint::new(hash!("d"), 0), make_txout(2)),
1278+
]
1279+
.into();
1280+
tx_update.anchors = [
1281+
(ConfirmationBlockTime::default(), hash!("a")),
1282+
(ConfirmationBlockTime::default(), hash!("b")),
1283+
]
1284+
.into();
1285+
tx_update.seen_ats = [(hash!("c"), 12346)].into_iter().collect();
1286+
tx_update
1287+
}),
12971288
];
12981289

12991290
for (test_name, update) in test_cases {
13001291
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();
1301-
let _ = tx_graph.apply_update_at(update.clone(), None);
1292+
let _ = tx_graph.apply_update(update.clone());
13021293
let update_from_tx_graph: TxUpdate<ConfirmationBlockTime> = tx_graph.into();
13031294

13041295
assert_eq!(

0 commit comments

Comments
 (0)