Repository navigation
Add custom sync methods - #1012
thunderbiscuit wants to merge 2 commits into
Conversation
| .map(|spk| ((KeychainKind::Internal, i), spk)) | ||
| })); | ||
|
|
||
| let builder = BdkSyncRequest::builder() |
There was a problem hiding this comment.
Could we preserve the expected_spk_txids behavior that BDK Wallet’s normal start_sync_with_revealed_spks{,_at} path adds? Those expected txids are used by sync backends to detect previously-seen mempool txs that disappear from SPK history. As written, this custom SyncRequest builder only sets the chain tip and selected SPKs, so these selective sync APIs can silently miss eviction/replacement updates compared with the existing sync path.
There was a problem hiding this comment.
We totally could and it's fairly easy to add, but I'm just not sure those are as useful as we think they are. They sort of imply you have a faulty clock (or don't have access to one?), and you need to set the time directly.
The other methods simply take the time at the moment the builder is created using something like now() I think, and you get exactly the same stuff regarding evictions and whatnot.
There was a problem hiding this comment.
Rechecked the updated implementation and verified locally that the custom request preserves the chain tip and expected transaction IDs for a selected script. This addresses my original eviction-metadata concern. The concern was about expected_spk_txids, rather than exposing an explicit timestamp.
| Arc::new(SyncRequestBuilder(Mutex::new(Some(builder)))) | ||
| } | ||
|
|
||
| /// Create a partial [`SyncRequest`] for a specific set of derivation indices. |
There was a problem hiding this comment.
Since this is a new public FFI API, could we add tests that reveal a known set of external/internal addresses and assert the request contains only the selected indices / last N revealed SPKs? That would also help guard this custom builder against drifting from BDK Wallet’s normal sync-request semantics.
There was a problem hiding this comment.
+1 for adding tests. I just did that!
ceefaac to
2e9dc1a
Compare
2e9dc1a to
831fca7
Compare
831fca7 to
ebff0bf
Compare
ebff0bf to
5d88586
Compare
| if spk_index.get_descriptor(keychain).is_none() { | ||
| return Err(SpkIndexError::KeychainNotFound { keychain }); | ||
| } | ||
| if spk_index.last_revealed_index(keychain).is_none() { |
There was a problem hiding this comment.
Could we only error when the combined selection is empty? Example: externalCount = 5, internalCount = 5 currently fails when external addresses are revealed but no change addresses are.
There was a problem hiding this comment.
And maybe we could cover this with tests where only external addresses are revealed, and vice versa?
| fetchPrevTxouts = true | ||
| ) | ||
|
|
||
| assertEquals(7, counter.inspected, "Only the 5 external and 2 internal scripts should be synced") |
There was a problem hiding this comment.
Could we also assert which scripts were synced because checking only the count would also pass if we accidentally selected the first scripts instead of the last I think
| BdkSyncRequest::builder() | ||
| .chain_tip(chain_tip) | ||
| .spks_with_indexes(spks) | ||
| .expected_spk_txids(expected_spk_txids) |
There was a problem hiding this comment.
Might want to add a rust test checking that the custom request preserves the wallets chain tip and expected transaction IDs for the selected scripts to possibly would help catch regressions in the metadata needed for eviction tracking? Non-blocking
| /// an unrevealed index would be misleading: the wallet's indexer does not track it, so the | ||
| /// outputs of any transaction found there would not be recognized as the wallet's own. |
There was a problem hiding this comment.
Can we keep the “every index must have been revealed” requirement and drop the explanation about outputs not being recognized?
| external_indices: Option<Vec<u32>>, | ||
| internal_indices: Option<Vec<u32>>, |
There was a problem hiding this comment.
non blocking: what do you think about these using plain lists defaulting to empty and the count arguments default to zero, just since None has the same meaning, or is there a better reason we need the optional types?
| continue; | ||
| } | ||
| if spk_index.get_descriptor(keychain).is_none() { | ||
| return Err(SpkIndexError::KeychainNotFound { keychain }); |
There was a problem hiding this comment.
Is rejecting the internal keychain intentional for single keychain wallets? Upstream methods like reveal_next_address map Internal to External in that case so this behaves differently is all.
|
concept looking good, added some comments/questions. in original description we can update |
This PR introduces two new methods:
Wallet::start_sync_with_spk_indices(external_indices, internal_indices)start_sync_with_last_revealed_spks(external_count, internal_count)Note that when using these methods, invalid inputs can be provided, and so the methods actually return a Result (for example if you give an index you haven't revealed yet or if you ask for indices on a keychain the wallet doesn't have). This required a new error that doesn't exist upstream:
SpkIndexError.Why adding methods to the wallet instead of exposing the whole builder?
I looked at exposing
SyncRequest::builder()to the bindings instead because that could potentially be the most flexible option, but it turned out to be super clumsy:chain_tiptakes aCheckPoint, which the bindings do not expose. A request built without a tip produces no chain update, so confirmed transactions cannot be connected to the local chain and show up as unconfirmed. Exposing the CheckPoint is not impossible, but it's a linked list of unknown size so it ends up being a lot more than we probably want, especially that you don't actually need it, you just need to pass it to your builder!expected_spk_txidsis weird to compute. It's a reimplementation of internal bookkeeping that every caller would write, and keep in step with upstream if the definition of "relevant spk" changes. Getting it wrong, or skipping it, fails silently, and a dropped or replaced transaction just stays pending forever. No error, no missing-argument complaint. Also, the wallet already has the answer, so having each app recompute it is wasted effort.Keeping the entry point on
Walletmeans both are always filled in. The cost is that we own two methods upstream doesn't have.Notes on API shape
There is a bit of bikeshedding to be done if you want to around certain choices I made:
None, so you can call a sync for the last 3 addresses on your external keychain like so:But for that you'll get an error (can't construct an empty
SyncRequest)3.
Noneis a bit different than an empty list, and if you try to sync an empty list on an internal keychain but you have a single-keychain wallet you get an error.Documentation
There are no direct upstream equivalent to these methods.
Checklists
All Submissions:
cargo fmtandcargo clippybefore committingchangelog:*labelNew Features: