fix(bdk_electrum_streaming): Keep the highest last active index per notification - #21
Merged
Merged
Conversation
Pin two properties of `last_active_indices` with state-level tests: - The emitted index must be the derivation index of the spk that has history, not the index after it (the off-by-one issue #3 reported). Already fixed on `main`; this pins it against a regression. - The keychain's last active index must be the *highest* of any spks that have history, no matter what order the server notifies their statuses in. Electrum notifies per-script-hash independently of our derivation order, so a later-derived spk can be reported before an earlier one. `state.rs`'s `on_spk_status` currently overwrites the staged index with whichever notification lands last, so this second test fails on `main`: two notifications for indices 4 then 3 leave the keychain's index at 3, not 4. Reporting a lower index than the actual highest leaves the higher spk unrevealed, so the wallet does not recognise its txouts as its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FAFd2PPjDZAfgP35zeQNN
…otification `on_spk_status` stages a keychain's last active index with a plain `BTreeMap::insert` whenever a script hash notification (or subscribe response) names history. Electrum notifies each subscribed script hash independently, in an order unrelated to derivation index, so a spk derived later can be notified before one derived earlier. The last notification processed wins, so the staged index can end up lower than the highest spk that actually has history. That index is what `reveal_to_target_multi` reveals to, so the higher spk stays unrevealed and the wallet does not recognise its txouts as its own. Merge on the maximum instead. `poll_spk_jobs` had its own write to the same field, derived from mere job completion rather than from a notification naming history. That made it fire for every finished spk job, active or not — including every empty look-ahead spk subscribed alongside the active ones, which is the more common case. `on_spk_status` already stages the index for every spk that actually has history, before its job even runs, so this second write was both redundant for active spks and actively wrong for inactive ones. Removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017FAFd2PPjDZAfgP35zeQNN
evanlinjin
force-pushed
the
claude/github-issue-3-status-0ewl7a
branch
from
September 1, 2026 12:37
57a7c00 to
40e0695
Compare
1 task
evanlinjin
added a commit
that referenced
this pull request
Sep 1, 2026
271f195 docs(bdk_electrum_streaming): Fix stale README and Cargo.toml description (志宇) 144e6e4 chore: Bump `bdk_electrum_streaming` to `v0.7.0` (志宇) Pull request description: ## Summary - Bumps `bdk_electrum_streaming` from `0.6.0` to `0.7.0` following the `last_active_indices` fix in #21, which changes the emitted result for keychains with multiple active spks. - Fixes the crate's README and `Cargo.toml` description, both of which were stale (the README referenced a dependency, `e_electrum_client`, that isn't the crate's actual dependency, `electrum_streaming_client`, and didn't say what the crate does). ## Test plan - [x] `cargo check -p bdk_electrum_streaming` 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017FAFd2PPjDZAfgP35zeQNN Top commit has no ACKs. Tree-SHA512: 0e9f85cd5bfa7ec28893990751dc2b3f726fa0b1204a69050e9e98705a54e8d3cf9460c0c141c7861d5b18da8a742e988e47407b7e8f3372af36cf22421bbfae
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Follow-up on #3 ("Fix off-by-one error in last_active_index emission").
The off-by-one from #3 is already fixed. At the time the issue was filed,
handle_script_statusreturnedthis_index + 1and that value was emitted directly as the keychain's last active index. Commit 4447f17 ("fix: Off-by-onelast_active_indicesupdate", May 2025) changed it to returnthis_index, and every emission site onmaintoday reports the index of the spk that actually has history. The first commit here adds a state-level regression test that pins that behaviour so it cannot silently regress.While covering it, I found a live bug with the same consequence (an active spk never gets revealed) — not an off-by-one, and not the one this PR originally described.
This PR was rebased onto
mainafter the "Give the chain and anchors one owner" refactor (#13) and theState::startrefactor (#17) landed. Those refactors movedlast_active_indicesbookkeeping fromadvance_spk_jobs/an inline notification handler into a unifiedon_spk_status+poll_spk_jobs. The original bug this PR fixed (advance_spk_jobsfolding several job completions into one update viaBTreeMap::extend, iterated in script-hash order) no longer exists in that form — but the refactor introduced a different bug with the same symptom, described below.The bug on current
mainon_spk_statusstages a keychain's last active index with a plainBTreeMap::insert(k, i)whenever a script hash notification (or subscribe response) names history. Electrum notifies each subscribed script hash independently, in an order unrelated to derivation index — a later-derived spk can be notified before an earlier one. The last notification processed wins, so the staged index can end up lower than the highest spk that actually has history. That index is whatreveal_to_target_multireveals to, so the higher spk stays unrevealed and the wallet does not recognise its txouts as its own.Separately,
poll_spk_jobshad its own write to the same field, derived from mere job completion rather than from a notification naming history. That made it fire for every finished spk job, active or not — including every empty look-ahead spk subscribed alongside the active ones, which is the common case.on_spk_statusalready stages the index for every spk that actually has history, before its job even runs, so this second write was both redundant for active spks and actively wrong for inactive ones (it would tag an empty look-ahead spk as "active" at whatever index it happened to be, clobbering the real one). Removed.Changes
test(bdk_electrum_streaming): Cover last_active_indices emissionAdds two state-level tests:
last_active_index_is_index_of_active_spk: pins the off-by-one fix (passes onmain).last_active_index_is_highest_regardless_of_notification_order: drives a sync where two spks of the same keychain (indices 3 and 4) each have history, and delivers their script-hash notifications with the higher index notified first. Fails onmainwithleft: [("external", 3)], right: [("external", 4)].fix(bdk_electrum_streaming): Keep the highest last active index per notificationMakes
on_spk_status's insert a max-merge instead of an overwrite, and removes the redundant/harmful write inpoll_spk_jobs. Passes with both tests above.Testing
cargo fmt --check,cargo clippy --lib --tests -D warnings, andcargo test(lib +tests/state.rs) all pass.tests/env.rscould not be run here: itsbdk_testenvdev-dependency buildsbitcoind, whose build script downloads Bitcoin Core frombitcoincore.org, which this sandbox's egress proxy blocks. That file is untouched by this PR, but it is worth a CI run.Generated by Claude Code