Repository navigation
perf(onchain): reuse a synced wallet across composes - #188
Conversation
compose_transaction built a fresh in-memory BDK wallet and ran a full Electrum sync on every call. Against the staging regtest Electrum with the Trezor emulator account (106 txs) that is about 7.4 s per compose, almost all of it in the sync. Keep the synced wallet per extended key, script type, network and Electrum URL. Later composes run an incremental sync that reuses the stored transactions, then build PSBTs on a copy of the synced database so the cached wallet's change index is never advanced. The cache key leaves out the fingerprint, so fee-estimate composes and signing composes share a wallet. A failed sync drops the cached wallet; at most 8 wallets are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
talosmachina
left a comment
There was a problem hiding this comment.
1 finding (1 P1). Keeps the synced watch-only BDK wallet per descriptor, network and Electrum URL, so later composes run an incremental sync and build PSBTs on a copy of its database. Reviewed aa32fa4, full tier.
Build skipped: talos-pr-run could not check the author's permission on this repo with the token it holds, and the check fails closed. So this is a static review: the tests were not run here.
What I checked, and 6 candidates I ruled out
Read in full: wallet_cache.rs, the compose.rs changes, the new tests in tests.rs, and in implementation.rs the functions resolve_wallet_setup, create_wallet, sync_wallet, connect_electrum and run_account_info_blocking. In bdk 0.30.2, Wallet::sync and blockchain/script_sync.rs (into_db_update).
CI: green (Android and iOS bindgen, manifests, usdt-tests, wake-tests). No CI job runs modules::onchain.
Ruled out
- Stale UTXOs on a warm sync:
into_db_updatedeletes txs the server no longer reports, along with their UTXOs, and recomputesis_spentfrom every observed tx, so a spent input is not reused. - Change index leaking into the cache: PSBTs are built on
copy_database's copy, and sync keepsmax(db_index, sync_index). The test asserts the source index stays at 3. - Descriptor checksum mismatch on the copy: checksums are deliberately not copied, and the cached wallet is always built from the origin-less descriptors, so the two
Wallet::newcalls never conflict. - Fingerprint left out of the key: a key origin changes only the PSBT metadata, not the derived scripts, so both setups watch the same scripts.
- Eviction while a slot is in use: the caller's
Arckeeps the slot alive. The worst case is one extra cold sync, never shared state. - Poisoned slot after a panic:
catch_unwindinsidespawn_blockingpoisons the mutex, andsynced_database_snapshotresets the slot toNone, so the next compose starts cold.
Merge confidence: 3/5, the P1 on the shared lock during an untimed sync.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pwltr
left a comment
There was a problem hiding this comment.
Reviewed commit 1ea7ad2. No supported correctness findings.
Compose connections now use a 30-second socket timeout, addressing the earlier stalled-server concern. The added timeout and wallet-lock release regression passed (1 test). This is a per-operation timeout; retries and multiple requests can extend total compose time.
Review details
General and targeted validation: GPT-6.1 Sol / High.
Prior security: Kimi K3 / High.
Related to synonymdev/bitkit-ios#1187. App-side parts: synonymdev/bitkit-ios#1221 and synonymdev/bitkit-android#1785.
onchain_compose_transactionbuilt a fresh in-memory BDK wallet and ran a full Electrum sync on every call. This keeps the synced wallet so later composes for the same account only run an incremental sync.Description
onchain/wallet_cache.rs: a process-wide cache of synced watch-only wallets keyed by descriptors (extended key and script type, without key origin), network and Electrum URL. At most 8 wallets are kept; the least recently used one is dropped.Measured breakdown on the cold path (Trezor emulator
allseed, m/84'/1'/0', regtest):ssl://electrs.bitkit.stag0.blocktank.to:9999(iOS regtest default)tcp://127.0.0.1:60001The 7.3–7.5 s in the issue matches the staging electrs sync, not the local Docker stack.
Review round 1
stalled_server_fails_within_timeout_and_releases_wallet, which hangs without the timeout and passes with it.Preview
End-to-end
compose_transaction, release build, two runs each:Fees were identical across the three composes in each run.
QA Notes
cargo test --lib modules::onchain: 83 passed, 26 ignored (at 1ea7ad2).New tests in
onchain/tests.rs(compose_wallet_cache): database copy keeps scripts, UTXOs, txs, indexes and sync time; a PSBT built on the copy carries the fingerprint andm/84'/1'/0'/...paths, change goes to the next internal index and the source index is untouched; cache key ignores the fingerprint but not server or script type; LRU eviction.Ignored live test:
COMPOSE_CACHE_ELECTRUM_URL=<url> cargo test --lib compose_reuses_synced_wallet -- --ignored --nocapture(defaults to staging electrs). Run against staging and local Docker as above.Full
cargo test: 735 passed, 11 failed. All 11 are Blocktank tests that callapi.stag.blocktank.to, which was unreachable from this machine.cargo clippyandcargo fmt: no new warnings.In the iOS app: this commit cherry-picked onto
v0.8.0-rc11(the version bitkit-ios pins; the Swift bindings are unchanged), built withbuild_ios.shand linked as a local package into bitkit-ios at fix: compose a hardware-funded request once bitkit-ios#1221 (DebugE2E_BUILD, staging regtest, iPhone 17e simulator iOS 26.5, Trezor emulator over Bridge). Payment requests fromtools/paykit-fixture-sender(synonymdev/bitkit-e2e-tests), 1,000,000 sats each, paid from the Trezor:965774ae6fde8a53d19174f1fe5c6ca5dd964138ca18235d95e9826f19d7854c), so the cached wallet did not reuse the inputs spent by request 1.Gaps:
get_account_infostill does a full sync per call and does not use this cache.🤖 Generated with Claude Code