<!-- You can erase any parts of this template not applicable to your Pull Request. -->
### Description
Closes #2406. Thanks @evanlinjin.
Replaces `test_classify_locked_script_timelocks` with the simplified version @evanlinjin proposed in #2406, and renames it to `test_classify_locked_descriptor_timelocks` since it no longer parses the script.
`LocalChain::apply_changeset` could replace the genesis block. Genesis is now only checked when the changeset has no point of agreement below it (the only path that reaches height 0), and the chain extends from the existing genesis checkpoint.
`apply_changeset` returns a new `ApplyChangeSetError` instead of `ApplyBlockError`, since `MissingGenesis` doesn't apply to a chain that already has genesis.
The second commit fixes `insert_block`, which reported every `prev_blockhash` mismatch as "failed to remove block at height 0". It now returns `InsertBlockError`, and `AlterCheckPointError` is removed.
### Changelog notice
```
### Fixed
- `LocalChain::apply_changeset` no longer replaces or removes the genesis block. #2309
- `LocalChain::insert_block` reports `prev_blockhash` mismatches instead of a bogus genesis error.
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
* [x] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
With the `hashbrown` feature, `bdk_core`'s `HashMap`/`HashSet` are hashbrown's, on `std` builds too. These hash with `ahash`, which uses fixed, publicly known keys unless `ahash`'s `runtime-rng` or `compile-time-rng` feature is enabled somewhere in the dependency graph. With fixed keys, a malicious chain source can send transactions whose txids collide in the hash table and make `TxGraph` insertion quadratic.
This adds a `# Features` section to `bdk_core`'s crate docs listing every feature: `std` (default), `serde` and `hashbrown`. The `hashbrown` entry documents the fixed keys and how to randomize them: depend on `ahash = { version = "0.8", features = ["runtime-rng"] }` (or `compile-time-rng`) directly.
Docs only; no behaviour change.
### Changelog notice
- `bdk_core`: Document the crate's features, including that `hashbrown` hashes with fixed keys unless `ahash`'s `runtime-rng`/`compile-time-rng` is enabled. #2354
On Windows hosts, compiling the `filter_iter` example via `cargo build -p bdk_bitcoind_rpc --example filter_iter` failed because Cargo compiles package-level dev-dependencies for examples. This pulled in `bdk_testenv` -> `electrsd`, whose `build.rs` relies on `std::os::unix::fs::PermissionsExt` and fails on Windows.
`filter_iter` connects to an external `bitcoind` RPC instance and only used `bdk_testenv` to import `anyhow`.
This patch:
- Adds `anyhow = "1"` directly to `[dev-dependencies]` of `bdk_bitcoind_rpc`.
- Removes the indirect `bdk_testenv::anyhow` import in `filter_iter.rs`.
- Moves `bdk_testenv` under `[target.'cfg(not(windows))'.dev-dependencies]`.
- Conditionally gates the local integration/unit tests that use the `TestEnv` regtest harness on non-Windows targets.
### Verification
Tested locally on Windows (`x86_64-pc-windows-msvc`):
- `cargo build -p bdk_bitcoind_rpc --example filter_iter` compiles cleanly.
- `target/debug/examples/filter_iter.exe` runs and displays the expected RPC argument requirements.
- `cargo check --tests -p bdk_bitcoind_rpc` passes with 0 warnings.
- `cargo test -p bdk_bitcoind_rpc` passes.
- `cargo clippy -p bdk_bitcoind_rpc --all-targets` passes with 0 warnings.
- `cargo fmt --package bdk_bitcoind_rpc -- --check` passes.
### Checklists
- [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
fix(bitcoind_rpc): allow filter_iter example to compile on Windows
The filter_iter example previously imported anyhow indirectly from
bdk_testenv. Because Cargo includes package dev-dependencies when
building examples, having bdk_testenv as an unconditional
dev-dependency pulled in electrsd, whose build script relies on Unix
permissions (std::os::unix::fs::PermissionsExt) and fails on Windows.
This change:
- Adds anyhow directly to [dev-dependencies] for bdk_bitcoind_rpc.
- Removes the indirect bdk_testenv::anyhow import in filter_iter.rs.
- Moves bdk_testenv to [target.'cfg(not(windows))'.dev-dependencies].
- Conditionally gates the bdk_testenv-dependent tests on not(windows).
With the `hashbrown` feature, this crate's `HashMap`/`HashSet` are
hashbrown's (on `std` builds too), which hash with `ahash`. Unless
`ahash`'s `runtime-rng` or `compile-time-rng` feature is enabled
somewhere in the dependency graph, `ahash` uses fixed, publicly known
keys, so a malicious chain source can craft colliding txids and make
`TxGraph` insertion quadratic.
Document this, and how to randomize the keys by depending on `ahash`
directly with one of those features.
Fixes #2342
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/checkout/releases">actions/checkout's releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>block checking out fork pr for pull_request_target and workflow_run by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Bump actions/publish-immutable-action from 0.0.3 to 0.0.4 in the minor-actions-dependencies group across 1 directory by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/actions/checkout/pull/2458">actions/checkout#2458</a></li>
<li>Bump flatted from 3.3.1 to 3.4.2 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/actions/checkout/pull/2460">actions/checkout#2460</a></li>
<li>Bump js-yaml from 4.1.0 to 4.2.0 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/actions/checkout/pull/2461">actions/checkout#2461</a></li>
<li>Bump <code>@actions/core</code> and <code>@actions/tool-cache</code> and Remove uuid by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/actions/checkout/pull/2459">actions/checkout#2459</a></li>
<li>upgrade module to esm and update dependencies by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2463">actions/checkout#2463</a></li>
<li>Bump the minor-npm-dependencies group across 1 directory with 3 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/actions/checkout/pull/2462">actions/checkout#2462</a></li>
<li>getting ready for checkout v7 release by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2464">actions/checkout#2464</a></li>
<li>update error wording by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2467">actions/checkout#2467</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> made their first contribution in <a href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/checkout/compare/v6.0.3...v7.0.0">https://github.com/actions/checkout/compare/v6.0.3...v7.0.0</a></p>
<h2>v6.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li><strong>[BREAKING]</strong> backport <code>allow-unsafe-pr-checkout</code> to v6 by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2500">actions/checkout#2500</a></li>
<li>backport fixes to releases-v6 by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2527">actions/checkout#2527</a></li>
</ul>
<p><a href="https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/">https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/</a> for more details about this breaking change</p>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/checkout/compare/v6.0.3...v6.1.0">https://github.com/actions/checkout/compare/v6.0.3...v6.1.0</a></p>
<h2>v6.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Update changelog by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2357">actions/checkout#2357</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a href="https://github.com/yaananth"><code>@yaananth</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
<li>Fix checkout init for SHA-256 repositories by <a href="https://github.com/yaananth"><code>@yaananth</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>Update changelog for v6.0.3 by <a href="https://github.com/yaananth"><code>@yaananth</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2446">actions/checkout#2446</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/yaananth"><code>@yaananth</code></a> made their first contribution in <a href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/checkout/compare/v6...v6.0.3">https://github.com/actions/checkout/compare/v6...v6.0.3</a></p>
<h2>v6.0.2</h2>
<h2>What's Changed</h2>
<ul>
<li>Add orchestration_id to git user-agent when ACTIONS_ORCHESTRATION_ID is set by <a href="https://github.com/TingluoHuang"><code>@TingluoHuang</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2355">actions/checkout#2355</a></li>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/checkout/compare/v6.0.1...v6.0.2">https://github.com/actions/checkout/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Update all references from v5 and v4 to v6 by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2314">actions/checkout#2314</a></li>
<li>Add worktree support for persist-credentials includeIf by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
<li>Clarify v6 README by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2328">actions/checkout#2328</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v7.0.1</h2>
<ul>
<li>Skip running unsafe pr check if input is default by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2518">actions/checkout#2518</a></li>
<li>Trim only ascii whitespace for branch by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2521">actions/checkout#2521</a></li>
<li>Escape values passed to --unset by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2530">actions/checkout#2530</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v7.0.0</h2>
<ul>
<li>Block checking out fork PR for pull_request_target and workflow_run by <a href="https://github.com/aiqiaoy"><code>@aiqiaoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2454">actions/checkout#2454</a></li>
<li>Various dependency updates</li>
</ul>
<h2>v6.0.3</h2>
<ul>
<li>Fix checkout init for SHA-256 repositories by <a href="https://github.com/yaananth"><code>@yaananth</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2439">actions/checkout#2439</a></li>
<li>fix: expand merge commit SHA regex and add SHA-256 test cases by <a href="https://github.com/yaananth"><code>@yaananth</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2414">actions/checkout#2414</a></li>
</ul>
<h2>v6.0.2</h2>
<ul>
<li>Fix tag handling: preserve annotations and explicit fetch-tags by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2356">actions/checkout#2356</a></li>
</ul>
<h2>v6.0.1</h2>
<ul>
<li>Add worktree support for persist-credentials includeIf by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2327">actions/checkout#2327</a></li>
</ul>
<h2>v6.0.0</h2>
<ul>
<li>Persist creds to a separate file by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2286">actions/checkout#2286</a></li>
<li>Update README to include Node.js 24 support details and requirements by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2248">actions/checkout#2248</a></li>
</ul>
<h2>v5.0.1</h2>
<ul>
<li>Port v6 cleanup to v5 by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2301">actions/checkout#2301</a></li>
</ul>
<h2>v5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>v4.3.1</h2>
<ul>
<li>Port v6 cleanup to v4 by <a href="https://github.com/ericsciple"><code>@ericsciple</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2305">actions/checkout#2305</a></li>
</ul>
<h2>v4.3.0</h2>
<ul>
<li>docs: update README.md by <a href="https://github.com/motss"><code>@motss</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a href="https://github.com/mouismail"><code>@mouismail</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a href="https://github.com/benwells"><code>@benwells</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a href="https://github.com/joshmgross"><code>@joshmgross</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a href="https://github.com/nebuk89"><code>@nebuk89</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a href="https://github.com/TingluoHuang"><code>@TingluoHuang</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a href="https://github.com/salmanmkc"><code>@salmanmkc</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment variables by <a href="https://github.com/jww3"><code>@jww3</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a href="https://github.com/jww3"><code>@jww3</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a href="https://github.com/orhantoy"><code>@orhantoy</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/checkout/commit/3d3c42e5aac5ba805825da76410c181273ba90b1"><code>3d3c42e</code></a> prep v7.0.1 release (<a href="https://redirect.github.com/actions/checkout/issues/2531">#2531</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/28802689a136bfcdb721715abd713740beecbe07"><code>2880268</code></a> escape values passed to --unset (<a href="https://redirect.github.com/actions/checkout/issues/2530">#2530</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/12cd2235efa0937479335606d7c3ac9f6c0973b1"><code>12cd223</code></a> trim only ascii whitespace for branch (<a href="https://redirect.github.com/actions/checkout/issues/2521">#2521</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/62661c4e71a304b2823ed026347b8d34c3eac541"><code>62661c4</code></a> skip running unsafe pr check if input is default (<a href="https://redirect.github.com/actions/checkout/issues/2518">#2518</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/e8d4307400f9427dba7cb98e488d6ab85f1cec5f"><code>e8d4307</code></a> Bump the minor-actions-dependencies group with 2 updates (<a href="https://redirect.github.com/actions/checkout/issues/2499">#2499</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/631c942040754b6e095e929c1677c07e10ed4f87"><code>631c942</code></a> eslint 9 (<a href="https://redirect.github.com/actions/checkout/issues/2474">#2474</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/4f1f4aec02e41874fa0262ea8ff5172d7978ad1e"><code>4f1f4ae</code></a> Bump actions/upload-artifact from 4 to 7 (<a href="https://redirect.github.com/actions/checkout/issues/2476">#2476</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/ba097532fb203f7e88c9c3c0b899b49469908a92"><code>ba09753</code></a> Bump actions/checkout from 6 to 7 (<a href="https://redirect.github.com/actions/checkout/issues/2488">#2488</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/b9e0990d219a03df7633c93f6f005a8fecbcab22"><code>b9e0990</code></a> Bump docker/login-action from 3.3.0 to 4.2.0 (<a href="https://redirect.github.com/actions/checkout/issues/2479">#2479</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/e8cb398be4a550817e382abf69e4c12c76fce1f2"><code>e8cb398</code></a> Bump docker/build-push-action from 6.5.0 to 7.2.0 (<a href="https://redirect.github.com/actions/checkout/issues/2478">#2478</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/actions/checkout/compare/v6...v7">compare view</a></li>
</ul>
</details>
<br />
> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 6.0.0 to 7.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<p>⚠️ Due to migration issues with keybase, we are unable to update our keys under the <code>codecovsecurity</code> account. We have deleted the account and are using <code>codecovsecops</code> with the original gpg key</p>
<h2>What's Changed</h2>
<ul>
<li>ci: remove Enforce License Compliance workflow by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1950">codecov/codecov-action#1950</a></li>
<li>chore(release): 7.0.0 by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1957">codecov/codecov-action#1957</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v6.0.1...v7.0.0">https://github.com/codecov/codecov-action/compare/v6.0.1...v7.0.0</a></p>
<h2>v6.0.2</h2>
<p>This is a copy of the <code>v7.0.0</code> release to make updates easier</p>
<h2>What's Changed</h2>
<ul>
<li>ci: remove Enforce License Compliance workflow by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1950">codecov/codecov-action#1950</a></li>
<li>chore(release): 7.0.0 by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1957">codecov/codecov-action#1957</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v6.0.1...v6.0.2">https://github.com/codecov/codecov-action/compare/v6.0.1...v6.0.2</a></p>
<h2>v6.0.1</h2>
<h2>What's Changed</h2>
<ul>
<li>fix: prevent template injection in run: steps (VULN-1652) by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1947">codecov/codecov-action#1947</a></li>
<li>chore(release): 6.0.1 by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1949">codecov/codecov-action#1949</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v6.0.0...v6.0.1">https://github.com/codecov/codecov-action/compare/v6.0.0...v6.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's changelog</a>.</em></p>
<blockquote>
<h2>v5.5.2</h2>
<h3>What's Changed</h3>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2">https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2</a></p>
<h2>v5.5.1</h2>
<h3>What's Changed</h3>
<ul>
<li>fix: overwrite pr number on fork by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1871">codecov/codecov-action#1871</a></li>
<li>build(deps): bump actions/checkout from 4.2.2 to 5.0.0 by <code>@app/dependabot</code> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1868">codecov/codecov-action#1868</a></li>
<li>build(deps): bump github/codeql-action from 3.29.9 to 3.29.11 by <code>@app/dependabot</code> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1867">codecov/codecov-action#1867</a></li>
<li>fix: update to use local app/ dir by <a href="https://github.com/thomasrockhu-codecov"><code>@thomasrockhu-codecov</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1872">codecov/codecov-action#1872</a></li>
<li>docs: fix typo in README by <a href="https://github.com/datalater"><code>@datalater</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1866">codecov/codecov-action#1866</a></li>
<li>Document a <code>codecov-cli</code> version reference example by <a href="https://github.com/webknjaz"><code>@webknjaz</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1774">codecov/codecov-action#1774</a></li>
<li>build(deps): bump github/codeql-action from 3.28.18 to 3.29.9 by <code>@app/dependabot</code> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1861">codecov/codecov-action#1861</a></li>
<li>build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 by <code>@app/dependabot</code> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1833">codecov/codecov-action#1833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1">https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1</a></p>
<h2>v5.5.0</h2>
<h3>What's Changed</h3>
<ul>
<li>feat: upgrade wrapper to 0.2.4 by <a href="https://github.com/jviall"><code>@jviall</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1864">codecov/codecov-action#1864</a></li>
<li>Pin actions/github-script by Git SHA by <a href="https://github.com/martincostello"><code>@martincostello</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1859">codecov/codecov-action#1859</a></li>
<li>fix: check reqs exist by <a href="https://github.com/joseph-sentry"><code>@joseph-sentry</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1835">codecov/codecov-action#1835</a></li>
<li>fix: Typo in README by <a href="https://github.com/spalmurray"><code>@spalmurray</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1838">codecov/codecov-action#1838</a></li>
<li>docs: Refine OIDC docs by <a href="https://github.com/spalmurray"><code>@spalmurray</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1837">codecov/codecov-action#1837</a></li>
<li>build(deps): bump github/codeql-action from 3.28.17 to 3.28.18 by <code>@app/dependabot</code> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1829">codecov/codecov-action#1829</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0">https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0</a></p>
<h2>v5.4.3</h2>
<h3>What's Changed</h3>
<ul>
<li>build(deps): bump github/codeql-action from 3.28.13 to 3.28.17 by <code>@app/dependabot</code> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1822">codecov/codecov-action#1822</a></li>
<li>fix: OIDC on forks by <a href="https://github.com/joseph-sentry"><code>@joseph-sentry</code></a> in <a href="https://redirect.github.com/codecov/codecov-action/pull/1823">codecov/codecov-action#1823</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3">https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3</a></p>
<h2>v5.4.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/codecov/codecov-action/commit/fb8b3582c8e4def4969c97caa2f19720cb33a72f"><code>fb8b358</code></a> chore(release): 7.0.0 (<a href="https://redirect.github.com/codecov/codecov-action/issues/1957">#1957</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/ca0a928a4cb3911011e868128a5cd90437c12db1"><code>ca0a928</code></a> ci: remove Enforce License Compliance workflow (<a href="https://redirect.github.com/codecov/codecov-action/issues/1950">#1950</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/e79a6962e0d4c0c17b229090214935d2e33f8354"><code>e79a696</code></a> chore(release): 6.0.1 (<a href="https://redirect.github.com/codecov/codecov-action/issues/1949">#1949</a>)</li>
<li><a href="https://github.com/codecov/codecov-action/commit/51e64229ac331acb0d7f7b17c67423995f991c79"><code>51e6422</code></a> fix: prevent template injection in run: steps (VULN-1652) (<a href="https://redirect.github.com/codecov/codecov-action/issues/1947">#1947</a>)</li>
<li>See full diff in <a href="https://github.com/codecov/codecov-action/compare/57e3a136b779b570ffcdbf80b3bdc90e7fab3de2...fb8b3582c8e4def4969c97caa2f19720cb33a72f">compare view</a></li>
</ul>
</details>
<br />
Esplora `sync` and `full_scan` insert server-reported previous outputs into `TxUpdate::txouts` without fetching the parent transactions to verify them. If a parent isn't in the `TxGraph`, fee and sent/received figures rely on these unverified values.
This adds a "Trust assumptions" section to the `bdk_esplora` README saying so, and links `sync`/`full_scan` (blocking and async) to it. Docs only; no behaviour change.
### Changelog notice
#### Added
- `bdk_esplora`: document that the previous outputs `sync` and `full_scan` add to `TxUpdate::txouts` are server-reported and unverified (new "Trust assumptions" section in the crate docs). #2350
`SyncRequest`'s `iter_*` methods drain the request despite their names, and there is no way to inspect pending items without consuming them. This PR, as proposed in #2220:
- Adds read-only `spks()`, `txids()` and `outpoints()`.
- Adds consuming `drain_spks_with_expected_txids()`, `drain_txids()` and `drain_outpoints()`.
- Deprecates `iter_spks_with_expected_txids()`, `iter_txids()` and `iter_outpoints()` as wrappers around the `drain_*` methods, so existing callers keep working.
- Moves the electrum and esplora call sites to `drain_*`.
No new tests: the behaviour is evident from the signatures.
Fixes #2220
### Changelog notice
### Added
- `SyncRequest::spks`, `SyncRequest::txids` and `SyncRequest::outpoints` for read-only access to pending sync items.
- `SyncRequest::drain_spks_with_expected_txids`, `SyncRequest::drain_txids` and `SyncRequest::drain_outpoints` for consuming iteration.
### Deprecated
- `SyncRequest::iter_spks_with_expected_txids`, `SyncRequest::iter_txids` and `SyncRequest::iter_outpoints`; use the `drain_*` methods, or the read-only accessors.
### Checklists
**All Submissions**
- [x] I followed the contribution guidelines.
**New Features**
- [ ] I've added tests for the new feature.
- [x] I've added docs for the new feature.
Classifies outputs whose timelock hasn't matured yet as `Eligibility::Immature`, so they aren't counted as spendable balance. `classify_outpoints` and `balance` take a new `is_locked` predicate, evaluated on every output.
### Notes to the reviewers
The timelock lives in the output's descriptor, so `CanonicalView` can't evaluate it from chain data alone. The caller passes `is_locked` instead, the same way it passes `does_taint`. The wallet side will follow in a separate `bdk_wallet` PR.
Timelocked (CSV, CLTV) outputs share `Immature` with immature coinbase outputs, since neither is spendable by consensus. Callers that need to tell them apart can check `is_on_coinbase`. Other locked coins that aren't enforced by consensus, like frozen or reserved coins, shouldn't use `is_locked` and are expected to be handled in the wallet.
### Changelog notice
Breaking: `CanonicalView::classify_outpoints` and `balance` take a new `is_locked` predicate which classifies outputs with unmet timelocks as `Immature`.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### New Features:
* [x] I've added tests for the new feature
* [x] I've added docs for the new feature
#### Bugfixes:
* [x] This pull request breaks the existing API
* [ ] I've added tests to reproduce the issue which are now passing
* [ ] I'm linking the issue being fixed by this PR
Dmenec [Thu, 15 Oct 2026 14:22:53 +0000 (16:22 +0200)]
feat(chain)!: classify timelocked outputs as `Immature`
Add an `is_locked` predicate on `classify_outpoints`/`balance` so the caller marks outputs whose timelock has not matured yet. They are classified as `Immature`.
Adds two `just` recipes that check whether workspace crates build as they would once published:
* `verify-standalone <crate> [args]` (alias `vs`): packages the crate and builds the packaged copy outside the workspace, so sibling workspace crates resolve from crates.io instead of local paths. A failure usually means the crate uses something from a sibling crate that isn't released yet. Extra args are passed to `cargo build`.
* `verify-standalone-all [args]` (alias `vsa`): runs `verify-standalone --all-features` on every publishable workspace crate, stopping at the first failure. `--all-features` catches uses of unreleased APIs behind non-default features.
The registry cache and build artifacts are kept under the target directory, so repeat runs are fast.
### Notes to the reviewers
Neither recipe is wired into CI. On current `master`, `bdk_chain` and `bdk_bitcoind_rpc` fail because they use `bdk_core` APIs that aren't released yet, which is what the check is for.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Runs `verify-standalone` with `--all-features` on every publishable
workspace crate, so feature-gated uses of unreleased workspace APIs are
also caught. Stops at the first failure.
Added alias `vsa` for quick access.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Passing `parallel_requests = 0` to `bdk_esplora`, or `batch_size = 0` to `bdk_electrum`, caused `full_scan` and `sync` to skip all requests and return an empty `Ok`. This made the wallet appear synced despite missing transactions, without indicating that the scan or sync had failed.
### Notes to the reviewers
This change ensures that zero-valued request limits do not cause `full_scan` or `sync` to silently skip the requested work and return an apparently successful result.
The regression tests use an unreachable server endpoint to verify that the requested fetches are not skipped when the relevant parameter is zero. The Esplora tests cover `parallel_requests = 0`, while the Electrum tests cover `batch_size = 0` and `parallel_requests = 0`, including the separate fetch loops for script pubkeys, transaction IDs, and outpoints.
### Changelog notice
```text
### Fixed
- Prevent `full_scan` and `sync` from silently skipping requests when `parallel_requests` or `batch_size` is zero in `bdk_esplora` and `bdk_electrum.
```
### Checklists
#### All Submissions:
- [x] I followed the [[contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md).
yahia008 [Fri, 9 Oct 2026 13:54:26 +0000 (14:54 +0100)]
fix(electrum): treat batch_size of 0 as 1
A `batch_size` of 0 made `full_scan` and `sync` skip every request and
return an empty `Ok`, because `populate_with_spks` builds each batch with
`(0..batch_size)` and returns when the batch is empty.
Clamp it to 1 in `populate_with_spks`, document this on `full_scan` and
`sync`, and add a test covering both.
yahia008 [Fri, 9 Oct 2026 13:54:26 +0000 (14:54 +0100)]
fix(esplora): treat parallel_requests of 0 as 1
A `parallel_requests` of 0 made `full_scan` and `sync` skip every request
and return an empty `Ok`, because each fetch loop takes its next batch with
`.take(parallel_requests)` and stops when the batch is empty.
Clamp it to 1 in each fetch loop, for both the blocking and async clients,
as `stop_gap` already is. Document this on `full_scan` and `sync`, and add
tests covering the spk, txid and outpoint paths.
Reuse indexer_with_last_revealed_at in apply_changeset_clamps_out_of_range_index_and_derives_correct_spk to simplify the test setup. The helper is also moved above its first use for readability.
Follow-up to #2265.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
<!-- You can erase any parts of this template not applicable to your Pull Request. -->
### Description
Correct two misleading `Store` API comments:
- `Store::create` opens the file for both reading and writing.
- Creating a recovered store at an existing path fails; replacing the original file requires a separate operation, as the recovery example demonstrates.
### Notes to the reviewers
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
of the PR were done in a specific way -->
### Changelog notice
<!-- Notice the release manager should include in the release tag message changelog -->
<!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### New Features:
* [ ] I've added tests for the new feature
* [ ] I've added docs for the new feature
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [ ] I've added tests to reproduce the issue which are now passing
* [ ] I'm linking the issue being fixed by this PR
`fetch_tip_and_latest_blocks` cast the server's `usize` tip height with `height as u32`, so on 64-bit a height above `u32::MAX` was silently truncated. It now uses `u32::try_from` and returns `Error::Message` on overflow.
Fixes #2294.
### Changelog notice
- Fixed: an overflowing Electrum tip height now returns an error instead of being truncated (#2294).
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [ ] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
`BdkElectrumClient` pairs each batch request with its response using `zip`, without checking that the lengths match. With a short response, trailing scripts, txids and anchors are silently dropped while `sync` returns `Ok`. A short `batch_block_header` response makes `height_to_hash[&h]` panic.
This adds a `check_batch_len` helper, called after each of the five batch calls. It returns `Error::Message` on a length mismatch. No public API changes.
Fixes #2305
### Notes to the reviewers
The stock `electrum_client::Client` can't return a short batch (its `batch_call` waits for every request id), so this only hardens custom `ElectrumApi` implementations. No tests: a mock `ElectrumApi` takes ~180 lines of stubs to guard five one-line checks.
This issue was found by AI, and I used Claude Code to assist with this PR.
### Changelog notice
Fixed: `bdk_electrum` returns an error instead of silently truncating results or panicking when an Electrum batch response length doesn't match the request.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [ ] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
fix(electrum): check batch response lengths before pairing
`BdkElectrumClient` paired every batch request with its response via
`zip` without checking that the response had one entry per request. A
short response (possible with any non-stock `ElectrumApi` implementation)
silently dropped trailing scripts, txids and anchors while `sync` still
returned `Ok`, and a short `batch_block_header` response left
`height_to_hash` incomplete so `height_to_hash[&h]` panicked.
Add `check_batch_len` and call it after each of the five batch calls
(`batch_script_get_history` x3, `batch_block_header`,
`batch_transaction_get_merkle`), returning `Error::Message` on any
length mismatch, in line with the other server-misbehaviour checks in
this crate.
`bdk_esplora` pages each script pubkey's history by passing the previous page's last txid as the cursor, and stops only on a page of fewer than 25 txs. A server that ignores the cursor returns the same full page forever, so `sync`/`full_scan` never returns and memory grows without bound.
The loop now records every cursor it pages from and returns `InvalidResponse` when a full page ends on one it has already seen. This catches both a repeated page and a cycle (`A -> B -> A`). Applied to both `async_ext.rs` and `blocking_ext.rs`.
### Notes to the reviewers
- It returns an error rather than `break`ing, so that `sync` never reports a truncated history as success.
- There is no page cap, so long histories aren't truncated. A correct server never repeats a cursor, so the check can't trigger on one.
- The regression tests use a small `TcpListener` stub in `tests/common` that always returns the same page, since `TestEnv` can't fake a misbehaving server. `tokio`'s `time` dev-dep feature is for the async test's timeout.
### Changelog notice
- Fixed `bdk_esplora`'s pagination loop hanging when a chain source returns a page that does not advance the `last_seen` cursor.
### Checklists
#### All Submissions:
* [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
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
`populate_with_outpoints` discarded every `fetch_tx` error, so a connection/TLS/timeout failure made `sync` return `Ok` without data for the outpoint.
It now skips only on `Error::Protocol` and propagates other errors, matching `populate_with_txids`.
### Changelog notice
- `bdk_electrum`: `sync` now returns an error instead of silently skipping an outpoint when fetching its transaction fails for a non-protocol reason
### Checklists
#### All Submissions:
* [X] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [X] I've added tests to reproduce the issue which are now passing
* [X] I'm linking the issue being fixed by this PR
Overlapping descriptors stay a caller contract violation. The docs now say so (debug builds panic), and `reveal_next_spk` returns `None` instead of panicking in release.
### Notes to the reviewers
The regression test is gated on `cfg(not(debug_assertions))`, so it only runs with `--release` (not in CI).
### Changelog notice
* Fixed: `KeychainTxOutIndex::reveal_next_spk` no longer panics in release builds on overlapping descriptors.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
luhrhenz [Fri, 9 Oct 2026 14:43:14 +0000 (15:43 +0100)]
fix(esplora): stop paging when the server does not advance the cursor
The per-script-pubkey pagination loop paged forward by asking for the
transactions following the last txid of the previous page, but never
checked that the cursor moved. A server that answers every request with
the same full page of 25 transactions kept the cursor in place, so the
loop re-requested that page forever while appending to an unbounded
`Vec`, and `sync`/`full_scan` never returned.
Track every cursor paged from, and return `InvalidResponse` if a full
page ends on one we have already seen. This catches a cursor that repeats
straight away as well as one that cycles (`A -> B -> A`). Erroring rather
than breaking avoids silently handing the caller a truncated transaction
history, which is how `chain_update` already treats a server it cannot
reconcile with.
SpkTxOutIndex::insert_spk currently accepts a different script at an existing index. The forward lookup is overwritten while the old reverse lookup remains, so the two maps disagree.
This returns false without changing state when the index is already occupied, matching the existing behavior for an occupied script. The method documentation now covers both cases.
### Notes to the reviewers
Added regressions for both lookup directions, unused scripts and previously scanned outputs. A rejected insertion preserves the old output and used status, and the rejected script is not tracked.
Testing on the refreshed master base:
- Rust 1.85: bdk_chain with all features, 99 tests/doctests pass, 1 ignored.
- Changed-crate clippy with all targets and features passes with warnings denied.
- Workspace formatting check passes.
- Full workspace pre-push suite has not been rerun on this base.
### Changelog notice
Reject duplicate indices in SpkTxOutIndex::insert_spk without changing existing script or output state.
hassan [Fri, 2 Oct 2026 00:18:20 +0000 (05:48 +0530)]
fix(chain): reject an occupied index in insert_spk
Reject an occupied index before changing either lookup map. Preserve existing script, scanned-output and used-status mappings on a rejected insertion, matching the existing behavior for an occupied script.
志宇 [Thu, 15 Oct 2026 09:54:57 +0000 (09:54 +0000)]
fix(chain)!: report `prev_blockhash` mismatches from `insert_block`
`insert_block` mapped every `apply_changeset` error to an
`AlterCheckPointError` about removing genesis. The only reachable error
there is a `prev_blockhash` mismatch, so callers inserting an unlinked
`Header` were told genesis could not be removed.
`insert_block` now returns `InsertBlockError`, which reports either the
existing conflicting block or the `prev_blockhash` mismatch. With no
users left, `AlterCheckPointError` is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
志宇 [Thu, 15 Oct 2026 09:46:13 +0000 (09:46 +0000)]
fix(chain)!: reject changesets that alter the genesis block
`LocalChain::apply_changeset` could replace the genesis block when the
changeset contained a different block at height 0. Check genesis only
when there is no point of agreement below the changeset (the only path
that touches height 0) and extend from the existing genesis checkpoint.
`apply_changeset` now returns the new `ApplyChangeSetError`, since
`ApplyBlockError::MissingGenesis` does not apply to a chain that already
has a genesis. Replacing or removing genesis is reported as
`ApplyChangeSetError::CannotAlterGenesis`.
`from_changeset` now builds directly via `from_blocks`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documents that `apply_block` and `apply_block_relevant` don't validate the block (chain membership or merkle root), so the caller must supply it from a trusted source. No `debug_assert!`: a bad block comes from a bad source at runtime, where the assert is compiled out.
* [x] I've signed all my commits
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`KeychainTxOutIndex::apply_changeset` accepted `ChangeSet::last_revealed` values with no upper bound, allowing an index greater than `BIP32_MAX_INDEX` (2^31 - 1) to be stored, violating an invariant relied on by `next_index()`.
The changes in this PR address boundary issues around derivation indices:
1. **`apply_changeset`**: clamps `last_revealed` to `BIP32_MAX_INDEX` before storing it, instead of storing the raw value.
2. **`replenish_inner_index`**: fixes an off-by-one in the `stop_index` calculation that caused a panic (`"we just inserted it"`) when a keychain's derivation index was revealed exactly up to `BIP32_MAX_INDEX`. The exclusive upper bound was clamped to `BIP32_MAX_INDEX` instead of `BIP32_MAX_INDEX + 1`, silently excluding the boundary value itself.
3. **`lookahead_to_target`**: fixes an integer overflow panic when called with `target_index == u32::MAX`, and changes out of range targets to clamp and replenish up to `BIP32_MAX_INDEX` instead of silently doing nothing.
### Notes to the reviewers
This revisits the earlier implementation attempt (#1792) and implements the clamp-in-`apply_changeset` approach from that review discussion.
The second and third fixes were found while adding the boundary tests requested in review of an earlier version of this PR.
Also added a test documenting current saturation behavior: `reveal_next_spk`/`next_unused_spk` return the last revealed script with an empty `ChangeSet`, not `None`, once the index is saturated. This matches `reveal_next_spk`'s existing doc comment.
Went through the other index-taking methods (`spk_at_index`, `is_used`, `mark_used`, `unmark_used`) as well. These are plain lookups with no boundary-sensitive logic, so no dedicated tests were added there.
### Changelog notice
- Clamped `last_revealed` to `BIP32_MAX_INDEX` in `KeychainTxOutIndex::apply_changeset` to prevent it accepting an out-of-range derivation index.
- Fixed an off-by-one in `replenish_inner_index` that could panic when revealing up to `BIP32_MAX_INDEX`.
- Fixed an integer overflow in `lookahead_to_target` when called with `target_index == u32::MAX`, and changed out of range targets to clamp to `BIP32_MAX_INDEX` instead of silently doing nothing.
### 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 `just p` before committing
**Bugfixes**
- [ ] This pull request breaks the existing API
- [x] I've added tests to cover the issue which are now passing
- [x] I'm linking the issue being fixed by this PR
The `ToBlockHash` docs said extra fields the block hash doesn't commit to "must be ignored", implying they're allowed. They aren't: `CheckPoint` and `LocalChain` treat data with equal hashes as interchangeable. The docs now require checkpoint data to be fully determined by its block hash, with anything else stored outside the checkpoint.
The `BlockWithTime` test helper now commits to its time field.
### Changelog notice
```
### Changed
- Documented that `CheckPoint` data must be fully determined by its block hash.
```
### Checklists
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Alternative to #2322, taking the approach outlined in https://github.com/bitcoindevkit/bdk/pull/2322#issuecomment-6136246396. Addresses part of #2269.
- **`SeenTxs` now skips coinbase txs.** A coinbase cannot be in the mempool, so a `last_seen` value on one is bad data and must not make it canonical. This was only a `debug_assert!`, so release builds canonicalized it at `ChainPosition::Unconfirmed`. The check now mirrors the one in `LeftOverTxs`.
- **`CanonicalTxOut::is_mature` no longer asserts on a coinbase without a confirmation height.** It returns `false` (maturity cannot be proven), which is what release builds already did.
Unlike #2322, a coinbase that is an ancestor of a canonical tx, or is in `assume_canonical`, stays canonical even when no anchor is held for it. A canonical spend of a coinbase implies the coinbase is in the chain, and excluding it leaves the view with a canonical tx whose parent is missing.
### Notes to the reviewers
This does not change what `ChainPosition` such a coinbase resolves to: it is still `Unconfirmed`. How (or whether) to represent "canonical coinbase, no anchor held" is a breaking design question and is left for a separate issue.
The `is_mature` assert was reachable through `balance()` on master: an unanchored coinbase with an unspent output, pulled in by an unconfirmed spend of another of its outputs. The second new test scenario covers this.
### Changelog notice
- Fixed: a coinbase tx with a `last_seen` value is no longer canonicalized in release builds.
- Fixed: `CanonicalTxOut::is_mature` no longer panics in debug builds for a canonical coinbase with no known anchor; it returns `false`.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
志宇 [Thu, 15 Oct 2026 10:11:16 +0000 (10:11 +0000)]
docs(core): require `CheckPoint` data to be determined by its block hash
The `ToBlockHash` docs allowed data types with extra fields that the
block hash does not commit to. `CheckPoint` and `LocalChain` treat data
with equal hashes as interchangeable, so such fields could be silently
kept or dropped. State that the data must be fully determined by its
block hash and that anything else belongs outside the checkpoint.
Make the `BlockWithTime` test helper commit to its time field so it
follows the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds unit tests for `SyncRequest` in `bdk_core::spk_client`:
- the `inspect` callback sees progress *after* each spk/txid/outpoint is consumed.
- `expected_spk_txids` merges txids across calls for the same spk, and other spks get none.
A from-scratch reimplementation of bitcoindevkit/bdk#1640 on top of the current `master`.
`TestEnv::wait_until_electrum_sees_block` previously only took a `timeout` and returned as soon as Electrum emitted *any* new-block notification. That made it ambiguous which block was actually being waited for, and it could return before Electrum had caught up to the block the test cared about.
This PR makes the wait concrete:
- `wait_until_electrum_sees_block` now takes `block_height: usize` and `block_hash: Option<BlockHash>`, so callers state exactly which block Electrum must be aware of. If the notified tip has already advanced past `block_height`, the header at that height is fetched explicitly so the expected `block_hash` can still be verified.
- A new convenience wrapper `wait_until_electrum_tip_syncs_with_bitcoind` waits until Electrum's tip matches bitcoind's chain tip (both height and hash). All existing call sites are migrated to it, since in every case the intent was "wait until Electrum catches up to the freshly mined/reorged tip".
### Notes to the reviewers
- **Why subscribe instead of polling:** polling Electrs for the header at `block_height` and checking its hash does not guarantee the spk histories are current. Receiving a block-tip notification does, because of how Electrs indexes. This is captured in a `NOTE` in the code.
- **Tip advanced past target:** the original PR's reviewers asked what happens when the method is called after the chain tip has moved past the target height. This is handled explicitly: when the notified height is greater than `block_height`, the header at `block_height` is fetched directly to verify the hash, rather than relying on the notification header.
- **`trigger()` placement:** `electrsd.trigger()` is kept inside the polling loop (matching the previous `master` behaviour) so Electrs keeps getting nudged to re-index — this is important for reorgs, where the tip changes without a height change.
- The previous WIP artefacts from #1640 (a `// TODO: Fails here.` marker and a struck-through doc comment) are intentionally left out.
### Changelog notice
- Changed: `TestEnv::wait_until_electrum_sees_block` now takes a `block_height` and an optional `block_hash` (breaking change to `bdk_testenv`).
- Added: `TestEnv::wait_until_electrum_tip_syncs_with_bitcoind`.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
Claude [Mon, 15 Jun 2026 21:36:50 +0000 (21:36 +0000)]
refactor(testenv)!: make `wait_until_electrum_sees_block` concrete
Previously `TestEnv::wait_until_electrum_sees_block` only took a `timeout`
and returned as soon as Electrum emitted any new-block notification, so it
was not clear which block was actually being waited for.
Add `block_height` and an optional `block_hash` so callers state exactly
which block Electrum must be aware of. When the notified tip has already
advanced past `block_height`, the header at that height is fetched
explicitly so the expected `block_hash` can still be verified.
Introduce `TestEnv::wait_until_electrum_tip_syncs_with_bitcoind`, a
convenience wrapper that waits until Electrum's tip matches bitcoind's
chain tip (both height and hash), and migrate all existing call sites to
it.
This PR adds a check in `Emitter::mempool_at`: when a mempool transaction is fetched with `get_raw_transaction`, its computed txid must match the txid that was requested before it goes into `mempool_snapshot`. This stops an RPC response with the wrong transaction body from being emitted or cached under the requested txid. It also adds module-level documentation stating that `bdk_bitcoind_rpc` assumes a connection to a trusted `bitcoind` node.
This addresses only the missing txid-validation part of #2282, following discussions on #2283.
### Notes to the reviewers
- On a mismatch the check returns the existing `Error::UnexpectedStructure`, so it is non-breaking. A single mismatch fails the whole poll; callers retry on the next call.
- Only freshly fetched transactions are checked. Entries already in `mempool_snapshot` were checked when they were inserted.
- A txid does not commit to witness data, so a transaction served with altered witnesses is not detected. This is documented on `mempool_at`.
- Out of scope / possible follow-up: `next_block` does not verify that the returned block matches the requested hash, merkle root, or witness commitment.
- The test uses a mock `RpcApi` that implements only `call`. `serde`/`serde_json` come from the `bitcoincore_rpc::jsonrpc` re-exports, so no new dependencies are added.
### Changelog notice
- `Emitter::mempool`/`mempool_at`: when a mempool transaction is fetched from the node, check that its computed txid matches the requested txid. A mismatch is rejected with `Error::UnexpectedStructure`.
- Document that `bdk_bitcoind_rpc` assumes a trusted `bitcoind` RPC connection.
<!-- Notice the release manager should include in the release tag message changelog -->
<!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### New Features:
* [x] I've added tests for the new feature
* [x] I've added docs for the new feature
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [x] I'm linking the issue being fixed by this PR
- Add `tx.compute_txid() == txid` check to validate that fetched raw
mempool transaction hashes against their requested txid before caching
in Emitter::mempool_at. Returns `Error::UnexpectedStructure` for a
mismatch.
- Add a regression test for the above check.
- Document that the crate assumes a trusted `bitcoind` connection, and
that the txid check does not cover witness data.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `Anchor` trait said an anchor block *might* be a descendant of the confirming block. Nothing produces such anchors: Electrum validates a merkle proof, Esplora uses the confirming block from `TxStatus`, and `TxPosInBlock` carries the tx index inside that block.
So an anchor's height is exact. This PR drops that framing from the `Anchor` docs and removes `Anchor::confirmation_height_upper_bound`. Callers use `anchor_block().height` directly. Every implementation already returned that, so there is no behaviour change.
`ChainPosition::confirmation_height_upper_bound` and `confirmations_lower_bound` are kept. A transitively confirmed tx (`ChainPosition::Confirmed { transitively: Some(_), .. }`) carries a descendant's anchor, so for it the height really is an upper bound. Their docs, along with those of `CanonicalTxOut::is_mature` / `is_confirmed_and_spendable` and `CanonicalView::balance`, now attribute the bound to transitive confirmation. `transitively_confirmed_tx_reports_descendant_anchor_height` covers this case.
### Changelog notice
**`bdk_chain`**
- Removed `Anchor::confirmation_height_upper_bound`. An anchor names the confirming block, so use `anchor_block().height`.
- Updated `Anchor` docs: an anchor block is no longer described as possibly a descendant of the confirming block.
- Updated `ChainPosition::confirmation_height_upper_bound` and `confirmations_lower_bound` docs: the values are exact unless the position is confirmed transitively.
- Updated `CanonicalView::balance` docs: a transitively confirmed output may be counted as pending by a confirmation-depth `is_settled`.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [x] This pull request breaks the existing API
* [x] I'm linking the issue being fixed by this PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
An anchor names the confirming block, so the confirmation height is
always `anchor_block().height`. An overridable method could only
diverge from that, so callers use `anchor_block().height` directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
志宇 [Tue, 13 Oct 2026 01:45:43 +0000 (01:45 +0000)]
fix(chain): skip coinbase txs with a `last_seen` in canonicalization
A coinbase tx cannot be in the mempool, so a `last_seen` value on one is
bad data and must not make it canonical. Previously this was only a
`debug_assert!`, so release builds canonicalized it as unconfirmed.
A coinbase that is an ancestor of a canonical tx, or is assumed
canonical, stays canonical even if no anchor is held for it. As that is
a reachable state, `CanonicalTxOut::is_mature` no longer asserts on it
and returns `false` as maturity cannot be proven.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
志宇 [Mon, 12 Oct 2026 08:49:54 +0000 (08:49 +0000)]
docs(chain): note false-pending in CanonicalView::balance
An output confirmed transitively carries a descendant's anchor, so its
confirmation height may be an upper bound. An `is_settled` predicate
that requires a minimum number of confirmations may therefore make
`balance` count it as pending even though it has enough.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An anchor names the block that confirmed the transaction, so its height
is exact, not an upper bound. Rename
`Anchor::confirmation_height_upper_bound` to
`Anchor::confirmation_height`.
This is a hard rename with no deprecated alias. The next `bdk_chain`
release is already breaking, so a downstream `Anchor` impl overriding
the old name should fail to compile rather than be silently ignored.
`ConfirmationBlockTime`'s override is removed as it was identical to the
default.
`ChainPosition::confirmation_height_upper_bound` and
`ChainPosition::confirmations_lower_bound` keep their names. A
transaction without its own anchor can be confirmed transitively during
canonicalization, in which case the position carries a descendant's
anchor and the height really is an upper bound. Their docs, and the
false-negative caveats on `CanonicalTxOut::is_mature` and
`is_confirmed_and_spendable`, now attribute the bound to transitive
confirmation instead of to the `Anchor` implementation. A test covers
the transitive case.
No behaviour change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NND8BhEmYTYdLRv4dbUUro Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs(chain): state that an anchor names the confirming block
The `Anchor` trait doc claimed an anchored transaction "could also mean
transaction A is confirmed in a parent block of B". No chain source in
this repo produces such an anchor: electrum only builds one after
`validate_merkle_proof` succeeds, esplora uses `TxStatus`'s confirming
block, and `TxPosInBlock` carries the transaction's index within that
block. The framing only leaves ambiguity for a case that does not occur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NND8BhEmYTYdLRv4dbUUro
The MSRV (1.85.0) CI job is failing on `master` and on every open PR. `proptest` 1.12.0 declares `rust-version = "1.88"`, and since there is no committed lockfile, CI resolves to it:
```
error: rustc 1.85.0 is not supported by the following package:
proptest@1.12.0 requires rustc 1.88
```
`proptest` is a dev-dependency of `bdk_core` and `bdk_chain` (`proptest = "1.2.0"`). This PR pins it to 1.11.0 in `ci/pin-msrv.sh`, the latest release that declares `rust-version = "1.85"`.
Failing run on `master`: https://github.com/bitcoindevkit/bdk/actions/runs/38205608516/job/114668304248
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
`block_header_cache` was only written by `batch_fetch_anchors`, so after a reorg it kept the pre-reorg header for that height. A tx re-mined at the same height then hit the anchor cache under the stale block hash, skipped proof validation, and was never re-anchored to the replacement block. This happens even for a 1-block reorg.
`fetch_tip_and_latest_blocks` now writes the headers it fetches (the chain suffix and the agreement-walk checkpoints) into the header cache.
### Notes to the reviewers
Only differences from #2319: the `fetch_tip_and_latest_blocks` signature keeps master's `CheckPoint<BlockHash>`, and the test uses master's `TestEnv` / `LocalChain` APIs. The test fails on `master` without the fix.
Related: #2312.
### Changelog notice
- Fixed `bdk_electrum` returning a stale anchor for a transaction re-mined at the same height after a reorg.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [ ] I'm linking the issue being fixed by this PR
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Remove the stale MSRV section from `crates/electrum/README.md`, which still claims Rust 1.75.0 and includes an obsolete dependency pinning command.
The root README already documents the current MSRV and pinning instructions. Removing the duplicate section reduces maintenance and makes the Electrum README consistent with the other crate READMEs.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
artofbitcoin [Sat, 3 Oct 2026 15:34:45 +0000 (17:34 +0200)]
docs(electrum): drop stale MSRV section from README
The root README already documents the current MSRV and dependency
pinning instructions. Remove the outdated duplicate section to reduce
maintenance and follow the other crate READMEs.
fix(electrum): propagate transport errors when syncing outpoints
`populate_with_outpoints` discarded every error returned by `fetch_tx`,
so a connection, TLS or timeout failure made `sync` report success
without any data for the requested outpoint. A spend of that outpoint
was then missed and the UTXO kept being treated as unspent.
Skip the outpoint only on `Error::Protocol` and propagate all other
errors, matching `populate_with_txids`.
test(electrum): cover tx re-mined at the same height after a reorg
Checks the tx is re-anchored to the replacement block both when the
reorged height is still in the synced chain suffix and when 20 blocks are
mined before the next sync.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fix(electrum): refresh header cache with headers fetched for the chain update
`block_header_cache` was only written by `batch_fetch_anchors`, so after a
reorg it kept serving the pre-reorg header for that height. The anchor
cache is keyed by `(txid, hash)` from that header, so a tx re-mined at the
same height hit the anchor cache with the stale hash, skipped proof
validation, and was never re-anchored to the replacement block.
`fetch_tip_and_latest_blocks` now writes the headers it fetches (the
chain suffix and the agreement-walk checkpoints) into the header cache.
The MSRV (1.85.0) CI job is failing. `yoke-derive` 0.8.3 (released 2026-09-15) calls `str::from_utf8` as a method on the `str` type. That form isn't available on Rust 1.85.0, and the crate doesn't declare a `rust-version`, so cargo doesn't avoid it:
```
error[E0599]: no function or associated item named `from_utf8` found for type `str` in the current scope
--> yoke-derive-0.8.3/src/lib.rs:202:32
|
202 | let underscores = str::from_utf8(&underscores).expect("_ is ASCII and thus UTF-8");
| ^^^^^^^^^ function or associated item not found in `str`
```
`yoke-derive` gets pulled in through `url` -> `idna` -> the `icu_*` crates -> `zerovec` -> `yoke`. This PR pins it to 0.8.2 in `ci/pin-msrv.sh`. Version 0.8.2 calls `core::str::from_utf8` instead, and `yoke` 0.8.3 accepts `yoke-derive ^0.8.2`.
I couldn't build with 1.85.0 locally, so the MSRV CI run on this PR is the real test. I did check that `ci/pin-msrv.sh` runs cleanly from a fresh lockfile and ends up with `yoke-derive` 0.8.2. The last green `master` run (2026-09-12) was before 0.8.3 came out.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Takes over #2235 (thanks @evanlinjin for the go-ahead). It reworks `CanonicalView::balance` to derive trust from an output's unconfirmed ancestry, and adds `classify_outpoints`, a per-output spend-eligibility classifier that `balance` becomes a thin fold over.
The old `balance` decided trust using a per-output `trust_predicate`, which cannot express transitive trust. As a result, owned outputs whose unconfirmed ancestry included foreign coins were counted as trusted. That is the root cause of the wallet trust-classification bugs (bitcoindevkit/bdk_wallet#16, bitcoindevkit/bdk_wallet#273).
The API now takes two separate predicates, one per concern:
- `does_taint(&tx)` - should this transaction be considered tainted? (e.g., because it spends a foreign unconfirmed output)
- `is_settled(&pos)` - do we consider this chain position settled / final? (generalizes `min_confirmations`)
For each unspent output, `classify_outpoints` reports its chain-level spend eligibility:
- `Settled` if considered settled according to `is_settled`
- `Immature` for a coinbase output that has not yet matured
- `Unsettled(Trust)` otherwise, where `Trust` is:
- `Trusted` if the whole unconfirmed ancestry only spends owned coins
- `Untrusted` if the output itself, or any unconfirmed ancestor, is tainted
- `Unknown` if part of the ancestry is missing from the view, so trust can't be determined
`balance` then sums each output's value into the bucket corresponding to its `Eligibility`.
### Notes to the reviewers
- Trust is resolved through a self-contained ancestry walk. The traversal is memoized: each visited transaction is cached so that already-classified transactions (and their ancestors) do not need to be walked again.
- Kept `Balance::confirmed` and did not rename it to `settled`. That rename is out of scope here. The folding logic is slated to move into `bdk_wallet`, and the current `balance` function in chain will eventually be deprecated.
- `balance` also drops the `O` generic and now takes plain `OutPoint`s, since the taint predicate operates on transactions rather than per-outpoint associated data.
- `does_taint` is evaluated at most once per transaction.
- More sophisticated classification rules (e.g., for coin control or locked funds) can be built on top of `classify_outpoints`. Left for a follow-up.
### Changelog notice
- **Breaking**: `CanonicalView::balance` now takes `does_taint: impl FnMut(&CanonicalTx) -> bool` and `is_settled: impl Fn(&ChainPosition) -> bool` instead of a per-output trust predicate and `min_confirmations`, and plain `OutPoint`s instead of `(identifier, outpoint)` pairs (this drops the `O` generic). Trust is now derived from an output's unconfirmed ancestry.
- **Breaking**: added `Balance::unknown_pending`, where outputs whose trust can't be determined are counted instead of being lumped into `untrusted_pending`.
- Added `CanonicalView::classify_outpoints` and the `Eligibility` enum.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
* [x] I ran `just p` before pushing
#### New Features:
* [x] I've added tests for the new feature
* [x] I've added docs for the new feature
fix(chain): fix integer overflow in lookahead_to_target
lookahead_to_target treated an out-of-range target_index as a
silent no-op, which is wrong — it should clamp to BIP32_MAX_INDEX
and replenish up to that point, matching reveal_to_target's
best-effort semantics.
Use saturating_add/saturating_sub, which no longer needs checked
arithmetic since replenish_inner_index's stop_index now clamps
correctly on its own.
fix(chain): fix off-by-one in replenish_inner_index stop bound
replenish_inner_index clamped stop_index (an exclusive bound) to
BIP32_MAX_INDEX, excluding the boundary itself and causing a panic
when a keychain's index was revealed exactly up to BIP32_MAX_INDEX.
The addition feeding into that clamp was also unchecked, risking
overflow if lookahead was large relative to an already-high
last_revealed.
Use saturating_add throughout, clamping to BIP32_MAX_INDEX + 1 so
the boundary value is included as documented.
test(chain): cover taint propagation and eligibility classification
- taint propagates through a pending output's unsettled ancestry
- is_settled alone decides the settled boundary, even for unconfirmed outputs
- taint never crosses a settled ancestor
- an immature coinbase is classified apart from a settled output
- two UTXOs sharing a tainting ancestor are both untrusted (shared taint cache)
- classify_outpoints skips spent and out-of-view outpoints
Co-authored-by: 志宇 <hello@evanlinjin.me> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(chain)!: derive balance trust from output ancestry
balance becomes a fold over classify_outpoints. A pending output's trust is now
taken from its unsettled ancestry (untrusted if an unsettled ancestor taints or
is missing from the view), not from a per-output flag.
BREAKING CHANGE: balance takes does_taint and is_settled instead of trust_predicate
and min_confirmations, and plain OutPoints instead of (identifier, outpoint)
pairs.
Co-authored-by: Dmenec <dmenec@proton.me> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds `.github/workflows/stale.yml`, based on the workflow introduced in the referenced PR, to automatically label and close inactive issues and abandoned pull requests in this repository.
The workflow uses `actions/stale@v11`
- Issues inactive for 180 days -> marked stale -> closed after 30 additional days
- Pull requests inactive for 180 days -> marked stale -> closed after 30 additional days
- Issue exemptions: `release`, `security`, `audit`, `do-not-close`, and `good first issue`
- Pull request exemptions: `blocked`, `security`, and `do-not-close`
- Issues and pull requests associated with milestones are exempt
- Runs weekly on Mondays at 09:00 UTC
- Pull request runs are automatically forced into dry-run mode for safe validation
- Stale labels are removed when new activity occurs
The timing and exemption policy are initially kept consistent with bitcoindevkit/bdk_wallet#497. They can be adjusted based on feedback.
### Notes to the reviewers
The workflow runs with `debug-only: true` on pull request events. I also recommend running it manually with `debug_only=true` after merging and reviewing the results before the first scheduled Monday run.
### Changelog notice
ci: add automated stale issue and pull request triage
### Checklists
#### All Submissions:
- [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
Suggested by @nymius in https://github.com/bitcoindevkit/bdk/pull/2246#discussion_r3796478212.
### Description
Adds `ChainPosition::confirmations_lower_bound(tip)`, which returns the number of blocks mined on top of a confirmed position's block, given the chain `tip`. Returns `None` if the position is unconfirmed, or if the confirmation height is above `tip`.
### Changelog notice
- Added `ChainPosition::confirmations_lower_bound`, returning the number of blocks mined on top of a confirmed position's block given the chain tip.
### 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 `just p` before committing
#### New Features:
* [x] I've added tests for the new feature
* [x] I've added docs for the new feature
feat(chain): classify outpoints by chain-level spend eligibility
Adds classify_outpoints, which decides per-output whether it's settled,
immature, or pending (trusted, untrusted, or unknown) based on its unsettled
ancestry. Trust is resolved with a memoized ancestry walk: a tainting ancestor
makes it untrusted, one missing from the view makes it unknown, and ancestors
shared by several outputs are only walked once.
Co-authored-by: 志宇 <hello@evanlinjin.me> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`IndexedTxGraph::reindex` was a single pass over `TxGraph::full_txs`, which iterates a `HashMap`. That made it **nondeterministic**: the same graph reindexed twice could produce two different `last_revealed` results.
Each match inside `KeychainTxOutIndex::_index_txout` bumps `last_revealed` and then calls `replenish_inner_index`, whose stop index is `last_revealed + 1 + lookahead`. So a match *widens the derived window the remaining outputs are judged against*. An output far enough out to need that widening was skipped if it happened to be visited first, and no pass revisited it.
Concretely, with `lookahead` 1000 and an empty frontier the derived set is `0..1001`. Given one tx paying index 900 and another paying 1850: visited 900-first, the window grows to 1901 and both are found; visited 1850-first, 1850 is missed and stays missed. Same inputs, different index, decided by `HashMap` iteration order.
The usual objection — that callers should reveal before reindexing — does not hold. The lookahead exists precisely to catch indices the persisted frontier does not know about: a restored wallet, another signer on the same descriptor, an externally built PSBT paying one of our far indices. Whenever the lookahead does its job the frontier moves mid-walk, so the order dependence is present in the intended use, not just in misuse.
### The change
Add `Indexer::rescan`, handed the whole `TxGraph` and returning the indexing it produced:
The default implementation offers every full transaction and floating output to `index_tx` / `index_txout` exactly once — all an indexer needs when what it recognizes is fixed up front — and `IndexedTxGraph::reindex` becomes a call to it. `KeychainTxOutIndex` overrides it and looks repeatedly, stopping when a pass leaves its revealed frontier unmoved.
Whether to look more than once belongs to the indexer, because the indexer is the only thing that knows whether its recognition set can still grow. Two things follow from putting it there rather than in `IndexedTxGraph`:
- **An indexer that does not widen what it matches is walked exactly once**, as today. `SpkTxOutIndex` is unaffected. There is no convergence requirement imposed on implementors, and no way for a third-party indexer to be spun forever by a loop it never asked for.
- **`KeychainTxOutIndex` can key the loop on its own frontier** rather than on whether a changeset came back empty. That matters: the changeset also carries staged spk cache entries, which move *without* the frontier moving. A `changeset.is_empty()` loop therefore spends an extra full walk on the ordinary restore path. I measured this — with `persist_spks = true`, restoring via `from_changeset` with a correct frontier takes **1 pass** keyed on the frontier versus **2** keyed on changeset emptiness.
### Notes to the reviewers
**On the test shape.** The regression test uses **one** transaction with two of our outputs rather than two transactions. A two-transaction test would depend on graph walk order — the very thing that is unreliable — and so would pass a single-pass implementation about half the time, which is a test that fails to fail. `index_tx` walks `tx.output`, a `Vec`, in vout order, so a single tx paying a far index at vout 0 and a near one at vout 1, against an empty frontier, misses the far output on every pass on every run. It is deterministic by construction; I verified it fails against the old implementation on five consecutive runs (`left: Some(9)`, `right: Some(15)`).
**On cost.** `KeychainTxOutIndex::rescan` re-walks the whole graph per look, so it is O(looks × txs), with looks bounded by the number of distinct frontier advances plus one. Measured: the settled/restore case is 1 look; the recovery case in the test (empty frontier, far output only reachable after the near one lands) is 3. Making a later look re-examine only the outputs that did not already match would need the indexer to say *what* to re-offer rather than just *whether*, which is a bigger change than this fix.
**Trait change.** `rescan` is a defaulted method, so existing `Indexer` implementations keep compiling unchanged. It is generic over the anchor `A`, which makes `Indexer` no longer object-safe — nothing in the workspace uses `dyn Indexer`.
**Known adjacent gap, not addressed here.** `index_tx_graph_changeset` — used by `insert_tx`, `insert_txout`, `apply_update` and `apply_block_relevant` — is still a single pass and has the same order dependence. Feeding the one-transaction case above through `insert_tx` yields `last_revealed = Some(9)` and only one of the two outpoints, so a live wallet can drop the far UTXO until the next restart re-runs `reindex`. It needs the incremental paths to go through the same mechanism rather than their own copy; I kept this PR to `reindex` to stay reviewable, and am happy to follow up.
### Changelog notice
- Added: `Indexer::rescan`, which indexes an entire `TxGraph` and lets an implementation decide how many looks that takes. Defaulted, so existing implementations are unaffected.
- Fixed: `IndexedTxGraph::reindex` no longer depends on `HashMap` iteration order. It previously made a single pass and could miss outputs at derivation indices that only came into range after another output in the same walk advanced the lookahead; `KeychainTxOutIndex` now looks until its revealed frontier stops moving.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### New Features:
* [x] I've added tests for the new feature
* [x] I've added docs for the new feature
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
LLFourn [Wed, 19 Aug 2026 05:15:39 +0000 (15:15 +1000)]
fix(chain): let the indexer decide how far to rescan
`IndexedTxGraph::reindex` was a single pass over `TxGraph::full_txs`,
which iterates a `HashMap`, so the same graph reindexed twice could
produce two different `last_revealed` results. Each match inside
`KeychainTxOutIndex::_index_txout` bumps `last_revealed` and replenishes
the lookahead, widening the derived window the remaining outputs are
judged against. An output far enough out to need that widening was
skipped whenever it happened to be visited first, and no pass revisited
it.
This is not merely misuse of the API. The lookahead exists precisely to
catch indices the persisted frontier does not know about -- a restored
wallet, another signer on the same descriptor, an externally built PSBT
paying one of our far indices -- so whenever it does its job the frontier
moves mid-walk, and the order dependence is present in the intended use.
Add `Indexer::rescan`, which is handed the whole graph and returns what
indexing it produced. The default implementation offers every full
transaction and floating output once, which is all an indexer needs when
what it recognizes is fixed up front; `reindex` becomes a call to it.
`KeychainTxOutIndex` overrides it and looks repeatedly, stopping when a
pass leaves its revealed frontier unmoved.
Putting the loop behind the trait rather than in `IndexedTxGraph` keeps
the decision where the knowledge is. An indexer that does not widen
what it matches is walked exactly once, as before, so the looping
cannot leak into implementations that have no use for it. And
`KeychainTxOutIndex` can key the loop on its own frontier instead of on
whether a changeset came back empty: the changeset also carries staged
spk cache entries, which move without the frontier moving, so an
emptiness test spends an extra full walk on the ordinary restore path.
The regression test uses a single transaction with both outputs rather
than two transactions, because the graph's walk order is a `HashMap`
order: a two-transaction test would pass a single-pass implementation
about half the time. `index_tx` walks `tx.output` in vout order, so the
far index at vout 0 is always judged against the initial window and
always missed until the near index at vout 1 lifts the frontier.
`CanonicalStage::LeftOverTxs` doesn't filter evicted txs and can mark them canonical. So a stale-anchored tx that was evicted/replaced could still show as unconfirmed.
Example: a tx confirms, then a reorg leaves only a stale anchor. Leftover treats it as unconfirmed. If it's later RBF'd or missing from the mempool, leftover still marks it unconfirmed.
### Changelog notice
- Add `TxNode::is_evicted()` for determining mempool eviction state.
- Skip evicted transactions during leftover canonicalization.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
#### Bugfixes:
* [ ] This pull request breaks the existing API
* [x] I've added tests to reproduce the issue which are now passing
* [ ] I'm linking the issue being fixed by this PR
Replaces the monolithic `build-test` matrix job (which ran a 2-toolchain × 2-feature-set matrix across the whole workspace) with dedicated per-crate build jobs. Each crate uses named steps for every meaningful feature combination, making it easy to identify which specific crate and feature set broke in CI.
**Changes:**
- **Per-crate jobs**: `build-core`, `build-chain`, `build-bitcoind-rpc`, `build-electrum`, `build-esplora`, `build-file-store`, `build-testenv` — each with per-feature `cargo build` steps and a `cargo test --all-features` step
- **`msrv` job**: Simplified from a matrix to a single flat job targeting 1.85.0; runs `cargo build --workspace --all-targets` and `cargo test --workspace --lib --tests --all-features`
- **`check-no-std`**: Updated to check `bdk_core` and `bdk_chain` with no default features + with `hashbrown` and `miniscript`; cross-compilation against `thumbv6m-none-eabi` is stubbed out as usual to be addressed in a separate PR
Adds `# Example` rustdoc to `Emitter::next_block` and `Emitter::mempool`, which were flagged as missing in the #2006 review.
Originally this PR also removed `example_bitcoind_rpc_polling`, but #2006 has since merged and removed all example crates, so that part is dropped after rebase. Only the doc additions remain.
Part of the #2006 follow-up to document the components previously covered by the `examples/` crates.
Automated update to Github CI workflow `cont_integration.yml` by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action