`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
Replaces the iterator-based `CanonicalIter` with a two-phase sans-IO canonicalization pipeline, and introduces a generic `ChainQuery` trait in `bdk_core` to decouple canonicalization from chain sources.
**Old API:**
```rust
// Direct coupling between canonicalization logic and ChainOracle
let view = tx_graph.canonical_view(&chain, chain_tip, params)?;
```
**New API:**
```rust
// Option A: Two-phase (full control)
let canonical_txs = chain.canonicalize(tx_graph.canonical_task(tip, params));
let view = chain.canonicalize(canonical_txs.view_task(&tx_graph));
#### Phase 1: `CanonicalTask`
Determines which transactions are canonical by processing them in stages:
1. **Assumed txs** — transactions assumed canonical via `CanonicalParams`
2. **Anchored txs** — transactions anchored in the best chain (descending height)
3. **Seen txs** — unconfirmed transactions by descending last-seen time
4. **Remaining txs** — leftover anchored transactions not in the best chain
Produces a `CanonicalTxs<A>` containing each canonical transaction with its `CanonicalReason`.
#### Phase 2: `CanonicalViewTask`
Resolves `CanonicalReason`s into concrete `ChainPosition`s (confirmed height or unconfirmed with last-seen), producing the final `CanonicalView<A>`.
Both phases implement the `ChainQuery` trait, so any chain source can drive them via the same `next_query`/`resolve_query` loop.
#### Key structural changes
- **`ChainQuery` trait** added to `bdk_core` — a generic sans-IO interface (`next_query` → `resolve_query` → `finish`) for any algorithm that needs to verify blocks against a chain source.
- **`ChainOracle` trait removed** — replaced by `ChainQuery`. `LocalChain::canonicalize()` now drives any `ChainQuery` implementor.
- **`Canonical<A, P>` generic container** — `CanonicalTxs<A>` (phase 1 output) and `CanonicalView<A>` (phase 2 output) are type aliases over `Canonical<A, P>`.
- **Module split** — `canonical_view.rs` split into `canonical.rs` (types: `Canonical`, `CanonicalTx`, `CanonicalTxOut`) and `canonical_view_task.rs` (phase 2 task). `canonical_iter.rs` replaced by `canonical_task.rs`.
### Notes to the reviewers
The changes are split into multiple commits for easier review. Also depends on #2029.
### Changelog notice
```
### Added
- `bdk_core::ChainQuery` trait — generic sans-IO interface for chain verification queries
- `bdk_core::ChainRequest` / `ChainResponse` type aliases
- `CanonicalTask` — phase 1 sans-IO canonicalization (determines canonical txs)
- `CanonicalViewTask` — phase 2 sans-IO canonicalization (resolves chain positions)
- `Canonical<A, P>` generic container with `CanonicalTxs<A>` and `CanonicalView<A>` aliases
- `LocalChain::canonicalize()` — drives any `ChainQuery` implementor
- `LocalChain::canonical_view()` — convenience method for full two-phase canonicalization
### Changed
- **Breaking:** Replace `TxGraph::canonical_iter()` / `TxGraph::canonical_view()` with `TxGraph::canonical_task()`
- **Breaking:** Canonicalization now uses a two-phase sans-IO process via `ChainQuery`
- **Breaking:** `ChainQuery`, `ChainRequest`, `ChainResponse` have no generics (use `BlockId` directly)
- **Breaking:** Chain tip moved from `ChainRequest` to `ChainQuery::tip()`
### Removed
- **Breaking:** `ChainOracle` trait and all implementations
- **Breaking:** `CanonicalIter` type and `canonical_iter` module
- **Breaking:** `TxGraph::try_canonical_view()` and `TxGraph::canonical_view()` methods
- **Breaking:** `CanonicalView::new()` public constructor
```
### 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
Leonardo Lima [Mon, 16 Mar 2026 23:01:22 +0000 (20:01 -0300)]
fix(chain): position resolution for assumed txs
- add new `test_canonical_view_task.rs` to handle different scenarios
of chain position resolution.
- fixes the assumed canonical txs chain position resolution, especially for transitively
assumed canonical transactions, where there's an anchored/confirmed descendant.
志宇 [Fri, 13 Feb 2026 12:57:39 +0000 (12:57 +0000)]
refactor(chain)!: split canonicalization into two tasks with generic `Canonical<A, P>`
Separate concerns by splitting `CanonicalizationTask` into two phases:
1. `CanonicalTask` determines which transactions are canonical and why
(`CanonicalReason`), outputting `CanonicalTxs<A>`.
2. `CanonicalViewTask` resolves reasons into `ChainPosition`s (confirmed
vs unconfirmed), outputting `CanonicalView<A>`.
Make `Canonical<A, P>`, `CanonicalTx<P>`, and `FullTxOut<P>` generic over
the position type so the same structs serve both phases. Add
`LocalChain::canonical_view()` convenience method for the common two-step
pipeline.
Leonardo Lima [Tue, 19 May 2026 19:32:39 +0000 (16:32 -0300)]
refactor(chain)!: remove `CanonicalIter` APIs
The codebase has been updated to the new `LocalChain::canonical_view`
method. It's now safe to remove the `CanonicalIter` it's the old APIs
relying on it, eg. `try_canonical_view`.
Leonardo Lima [Tue, 19 May 2026 18:56:26 +0000 (15:56 -0300)]
chore(workspace): use new `LocalChain::canonical_view` API
Updates the codebase to use the new convenience
`LocalChain::canonical_view` method in order to generate the
`CanonicalView`. Internally the convenience method follows the `sans-IO` approach,
separating the canonicalization algorithm from i/o operations, and it's
used as follows:
1. Create a new `CanonicalizationTask` with a `TxGraph`, by calling:
`graph.canonicalization_task(params)`
2. Execute the canonicalization process with a chain oracle (e.g
`LocalChain`, which implements `ChainOracle` trait), by calling:
`chain.canonicalize(task, chain_tip)`
Leonardo Lima [Tue, 19 May 2026 18:53:10 +0000 (15:53 -0300)]
feat(core,chain): introduce `CanonicalizationTask` and `ChainQuery`
It introduces the new `CanonicalizationTask` that's implements the
canonicalization algorithm through a request/response pattern.
Also, it introduces the new `ChainQuery` trait in `bdk_core`, which
provides an interface for blockchain source/oracle query-based operations.
Allowing sans-IO patterns for algorithm that needs a blockchain oracle,
without the need for directly implement/handle I/O.
Adds new API methods into `LocalChain`: `canonicalize` and `canonical_view`,
adding same features as the existing `CanonicalIter` and it's APIs.
The PR is now scoped to the first step of the work discussed in #1973:
- Remove the unmaintained example binaries in examples/
- Remove the related workspace members from Cargo.toml
- Clean up docs and CI references that pointed to the removed examples
Add a skiplist to `CheckPoint` using Bitcoin Core's `CBlockIndex::pskip` pattern, adapted to operate on checkpoint indices rather than block heights. Result: O(log n) lookups for `get`, `floor_at`, and `range`, with no tuning constant. Per-node memory is unchanged — `Option<Arc<CPInner>>` is niche-optimized to 8 bytes whether or not it's populated.
The pskip approach was suggested by @ValuedMammal in https://github.com/bitcoindevkit/bdk/pull/2048#issuecomment-4328710770 and prototyped in https://github.com/ValuedMammal/block-graph/blob/master/block-graph/src/checkpoint.rs. Credit for the core idea goes there. This PR takes a different implementation path; the rationale is below.
#### Design
Every `CheckPoint` node carries one Arc skip pointer to a deterministically chosen ancestor at index `skip_index(i)`. The chosen targets give skip distances that grow exponentially as you walk back, yielding O(log n) traversal — ~17 hops at 1M blocks vs ~1M for a linear walk.
`get`, `floor_at`, and `range` all reduce to "walk back to the highest checkpoint at or below `target_height`", factored into one private `walk_to_floor` helper. Each public method is a thin wrapper.
#### Bench numbers (criterion)
Absolute pskip times, with a linear-walk baseline at 1M blocks for scale:
```
get_1000_middle 227 ns
get_10000_near_end 397 ns
floor_at_10000 565 ns
random_access_skiplist_1m 1.36 µs (vs 3.84 ms linear walk over the same chain)
```
#### Caveats
- Insert is ~50 ns slower per push than a non-skiplist `CheckPoint` because every push wires its skip pointer via an O(log n) `ancestor_by_index` walk. For real-world per-block chain extension this is negligible (well under 1 µs/block), but bulk-rebuild paths (`insert_sparse_1000` → ~3.5 µs for 1000 sequential pushes) pay it linearly.
### Notes to the reviewers
#### History
This PR went through three design iterations. Including the rationale here so reviewers don't have to reconstruct it from the commit graph:
**1. Fixed-stride skiplist (initial proposal).** First version added a single skip pointer every `CHECKPOINT_SKIP_INTERVAL` indices, each pointing exactly that many positions back. `INTERVAL` was originally `100`, then bumped to `1000` after benchmarking — at mainnet-scale chains (~1M blocks) the cost-optimal stride for an O(n/k + k) walk is `k ≈ √n ≈ 1000`. This delivered ~50–80× speedups over linear walks but still left lookups at O(√n) (~2,000 hops at 1M blocks) and forced a tuning constant.
**2. Bitcoin Core's `pskip` (current).** @ValuedMammal pointed out that Bitcoin Core's `CBlockIndex::pskip` pattern achieves O(log n) lookups with no tuning constant by giving every node a single skip pointer to a *deterministically chosen* ancestor (computed from the index's bit pattern). Crucially, this comes at no extra per-node memory cost: `Option<Arc<CPInner>>` was already 8 bytes per node thanks to niche-optimization, so populating the field on every node uses the same space as populating it on every 1000th. Switched to this approach; the synthetic skiplist benches went from ~80% improvement to ~94% over linear, and the tuning constant disappeared.
**3. Reuse the pskip walker.** Once `ancestor_by_index` (the O(log n) walk underlying pskip) existed, it became natural to plug it into anywhere `CheckPoint`/`CheckPointIter` was advancing through the chain by a known amount. `get`, `range`, `floor_at`, `floor_below`, and `Iterator::nth` / `last` all use it now; `count` and `size_hint` derive from the `index` counter directly. Unused traversal methods on `CheckPointEntry` were removed in the same pass since they were duplicating `CheckPoint`'s own surface and had no callers in the workspace.
#### Diffs from @ValuedMammal's prototype
The [block-graph prototype](https://github.com/ValuedMammal/block-graph/blob/master/block-graph/src/checkpoint.rs) gets the high-level idea right (every node carries one deterministic skip pointer; index-based formula so sparse chains work) but has a few choices I wanted to address differently:
**1. `n & (n - 1)` underflow.** The prototype handles it by converting `u32 → i32 → u32` with `try_into().expect(...)`, relying on signed two's-complement to give `0 & -1 == 0`. This PR uses `n & n.wrapping_sub(1)` directly on `u32`, which produces the same `0 & u32::MAX == 0` result without the round-trip conversion.
**2. Traversal heuristic.** The prototype's `get` uses simple greedy ("take skip if it doesn't undershoot, else prev"). That's correct but **not genuinely O(log n)** — for targets near genesis on a long chain, it degrades to many small geometric descents stacked end-to-end. This PR uses Bitcoin Core's `GetAncestor` heuristic verbatim: take skip unless the *predecessor's* skip would have been a strictly bigger jump that still doesn't undershoot.
**3. Single internal helper for `get`/`range`/`floor_at`.** The public surface (`get`, `range`, `floor_at`) all reduces to "walk back to the highest checkpoint at or below `target_height`". This PR factors that into one private `walk_to_floor` function; each public method becomes a thin wrapper. The prototype duplicates the skip-walk logic.
**4. `Drop` cleanup.** With every node holding a skip Arc, the manual unwind loop in `Drop for CPInner` (existing fix for #1634) is extended with `node.skip.take()` so skip-pointer drops happen on the manual loop rather than triggering ancestor drops via the implicit recursive path.
### Changelog
```md
Added:
- `O(log n)` skiplist on `CheckPoint` (Bitcoin Core's pskip pattern). Speeds up `get`, `floor_at`, `range`, and full-chain iteration.
- `Iterator::nth` / `last` / `count` / `size_hint` overrides on `CheckPointIter` and `CheckPointEntryIter`, plus `ExactSizeIterator` impls. `nth` and `last` are now `O(log n)`; `count` and `size_hint` are `O(1)`.
Removed:
- Unused traversal methods on `CheckPointEntry`: `iter`, `get`, `range`, `floor_at`, `floor_below`. They had no callers in the workspace and duplicated `CheckPoint`'s own surface.
```
### 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
Addresses review feedback from @nymius: existing benches use fixed
targets, which can land favorably or unfavorably relative to skip
pointer positions and don't reflect real query patterns. The new
bench draws 256 targets from a deterministic xorshift sequence and
runs both a skiplist-enhanced get() and a plain linear walk over
a 100k-node chain, so the same query stream exercises both paths.
Also changed benchmarks to reflect checkpoint full chains and removed
unhelpful benchmarks.
feat(core): add skiplist to CheckPoint for faster traversal
Adds a skip pointer and an index field to CheckPoint to accelerate
get(), floor_at(), and range(). push() and insert() maintain the
index/skip invariants on the rebuilt chain.
The skip pointer follows Bitcoin Core's `CBlockIndex::pskip` pattern,
adapted to operate on checkpoint indices (not heights) so it works on
sparse chains. Skip distances grow exponentially as you walk back,
yielding O(log n) traversal.
`BdkElectrumClient::populate_with_txids` queries each transaction's confirmation status by calling `script_get_history` on the script of one of its outputs. It currently picks the first output unconditionally. This breaks for transactions which first output is an `OP_RETURN`, because Electrum servers don't index `OP_RETURN` scripts and will return an empty history. This is a real-world scenario: protocols like RGB place an `OP_RETURN` commitment as the first output of every transaction.
### Notes to the reviewers
The fix selects the first output which script is not `OP_RETURN` or a `OP_FALSE OP_RETURN`. If a transaction has only `OP_RETURN`/`OP_FALSE OP_RETURN` outputs, we fall back to the script of any input's previous output to query history. The only case still skipped is a coinbase with all unindexed outputs, since coinbases have no parent to fall back on.
### Changelog notice
```
Fixed:
- `BdkElectrumClient::sync` now correctly retrieves confirmation status for transactions which first output is an `OP_RETURN` or `OP_FALSE OP_RETURN`
```
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
When a reorg drops the agreement point below `start_height`, the emitter would skip directly to `start_height`, producing a checkpoint that could not connect with the caller's local chain (`CannotConnectError`).
This affects callers that create a new `Emitter` on each sync with `start_height = tip_height` — a common pattern.
**Fix:** Override `start_height` to the agreement height when a reorg is detected (agreement point below both `start_height` and `last_cp`), so the emitter revisits the invalidated block heights.
The key change is in `poll()` in `crates/bitcoind_rpc/src/lib.rs`: when `AgreementFound` is handled and the agreement point is below both `start_height` and `last_cp.height()`, we lower `start_height` to the agreement height. This ensures the emitter emits the invalidated blocks instead of skipping over them.
### Changelog notice
```md
Fixed:
- `Emitter` producing un-connectable checkpoints when `start_height` is above the agreement point 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
* [x] I'm linking the issue being fixed by this PR
Fixes #2057. `full_scan` now always covers the revealed range before applying the stop_gap
### Notes to the reviewers
Replaces #2181. That PR unified full_scan and sync into one path; per @evanlinjin's feedback we're keeping them separate and doing the minimal fix here instead
### Changelog notice
Fixed: full_scan now always scans the full revealed range before applying stop_gap past last_revealed.
### 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
An Electrum server could return an arbitrary transaction when `fetch_tx()` requests a specific txid. The returned transaction was cached and used without verifying that its computed txid matches the requested one.
Add a verification check that `tx.compute_txid() == txid` after fetching from the server, returning an error on mismatch. Include a unit test with a mock Electrum client that exercises both the mismatch rejection and the matching-txid happy path.
Elias Rohrer [Thu, 23 Apr 2026 16:54:52 +0000 (18:54 +0200)]
fix(electrum): verify txid of server-returned transactions
An Electrum server could return an arbitrary transaction when
`fetch_tx()` requests a specific txid. The returned transaction was
cached and used without verifying that its computed txid matches the
requested one.
Add a verification check that `tx.compute_txid() == txid` after
fetching from the server, returning an error on mismatch.
fix(bitcoind_rpc): emit invalidated heights when start_height is above agreement point
When a reorg drops the agreement point below `start_height`, the emitter
would skip directly to `start_height`, producing a checkpoint that could
not connect with the caller's local chain (`CannotConnectError`).
Fix: override `start_height` to the agreement height when a reorg is
detected (agreement point below both `start_height` and `last_cp`), so
the emitter revisits the invalidated block heights.
The start and end bound calculations used unchecked addition which
overflows when given `u32::MAX`, causing the iterator to silently produce
wrong results (e.g., an empty iterator for `0..=u32::MAX` in release
mode). Use `saturating_add` to handle the boundary correctly.
### 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
Elias Rohrer [Wed, 8 Apr 2026 09:42:39 +0000 (11:42 +0200)]
fix(chain): prevent integer overflow in `SpkIterator::new_with_range`
The start and end bound calculations used unchecked addition which
overflows when given `u32::MAX`, causing the iterator to silently produce
wrong results (e.g., an empty iterator for `0..=u32::MAX` in release
mode). Use `saturating_add` to handle the boundary correctly.
Co-Authored-By: HAL 9000 Signed-off-by: Elias Rohrer <dev@tnull.de>
Closes #2021
Related to #2076
Replaces #2024
Replaces #2091
### Description
This PR adds `prev_blockhash` awareness to `CheckPoint`, enabling proper chain validation when merging checkpoint chains that store block headers or similar data with previous block hash information.
### Notes to the reviewers
This PR replaces some prior attempts:
* #2024 - where we made the `CheckPoint::data` optional - however this resulted in internal complexity and an API with annoying edge cases. The tests from this PR were still useful.
* #2091 - This second attempt had some good ideas, but was distracted from the goal of #2021. I mostly reused the `CheckPoint::insert` implementation of that PR.
### Changelog notice
```md
Added:
- `ToBlockHash::prev_blockhash()` - optional method to expose previous block hash
- `CheckPointEntry` - new type for iterating with `prev_blockhash` awareness, yielding "placeholder" entries for heights inferred from `prev_blockhash`
- `ApplyBlockError` - this is a new error type with two variants; `MissingGenesis` and `PrevBlockhashMismatch`. The second variant is a new error case introduced by `prev_blockhash` awareness.
Changed:
- `CheckPoint::push` - now errors when `prev_blockhash` conflicts with current tip (contiguous heights)
- `CheckPoint::insert` - now evicts/displaces checkpoints on `prev_blockhash` conflict
- `merge_chains` - now validates `prev_blockhash` consistency when merging
- `LocalChain<D>` generic parameter - relaxed constraint to `D: Clone` instead of `D: Copy`.
Fixed:
- `merge_chains` no longer replaces the genesis block.
```
### 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
docs: address review feedback on `prev_blockhash` validation
- Clarify `CheckPoint::insert` eviction semantics when `data.prev_blockhash`
conflicts with the checkpoint at `height - 1`.
- Explain placeholder handling in `merge_chains` conflict branch where
`u.data()` may legitimately insert `None`.
- Preserve mismatch height in `merge_chains`' fallback so release-mode
callers get a useful `try_include_height` instead of `0`.
- Reframe the "update displaces invalid block" test comment to make the
best-effort recovery intent explicit.
- Derive `PartialEq` on `CheckPointEntry` and fix a broken intra-doc link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(core): address review feedback on docs and tests
Address suggestions by @nymius:
- Add `Returns` and `Errors` doc sections to `CheckPoint::from_blocks`
- Add test assertions for chain integrity after failed push and chain
length after successful push
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
志宇 [Wed, 18 Feb 2026 17:33:29 +0000 (17:33 +0000)]
fix(chain)!: make genesis immutable in `merge_chains`
Prevent `merge_chains` from replacing the genesis block when original
and update disagree on the genesis hash. This aligns with
`CheckPoint::insert` which already panics on genesis replacement.
Also update the "fix blockhash before agreement point" test to operate
at a non-genesis height and add a new test for conflicting genesis.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
docs(core): Add module-level docs for `checkpoint_entry`
Explain the purpose of `CheckPointEntry` and its two variants:
- `Occupied`: real checkpoint at this height
- `Placeholder`: implied by `prev_blockhash` from checkpoint above
Also fix typo: "atleast" → "at least"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(chain)!: Add `ApplyBlockError` for `prev_blockhash` validation
Introduce `ApplyBlockError` enum with two variants:
- `MissingGenesis`: genesis block is missing or would be altered
- `PrevBlockhashMismatch`: block's `prev_blockhash` doesn't match expected
This replaces `MissingGenesisError` in several `LocalChain` methods:
- `from_blocks`
- `from_changeset`
- `apply_changeset`
Also adds test cases for `merge_chains` with `prev_blockhash` scenarios:
- Update displaces invalid block below point of agreement
- Update fills gap with matching `prev_blockhash`
- Cascading eviction through multiple blocks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(core): add tests for CheckPoint::push and insert methods
Add comprehensive tests for CheckPoint::push error cases:
- Push fails when height is not greater than current
- Push fails when prev_blockhash conflicts with self
- Push succeeds when prev_blockhash matches
Include tests for CheckPoint::insert conflict handling:
- Insert with conflicting prev_blockhash
- Insert purges conflicting tail
- Insert between conflicting checkpoints
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: valued mammal <valuedmammal@protonmail.com>
test(chain): make `TestLocalChain` generic and add `prev_blockhash` test
Make `TestLocalChain` and `ExpectedResult` generic over checkpoint data
type `D`, allowing the same test infrastructure to work with both
`BlockHash` and `TestBlock` types.
Add `merge_chains_with_prev_blockhash` test to verify that `prev_blockhash`
correctly invalidates conflicting blocks and connects disjoint chains.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Automated update to Github CI workflow `cont_integration.yml` by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 8.0.0 to 8.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/peter-evans/create-pull-request/releases">peter-evans/create-pull-request's releases</a>.</em></p>
<blockquote>
<h2>Create Pull Request v8.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>README.md: bump given GitHub actions to their latest versions by <a href="https://github.com/deining"><code>@deining</code></a> in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4265">peter-evans/create-pull-request#4265</a></li>
<li>build(deps): bump the github-actions group with 2 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4273">peter-evans/create-pull-request#4273</a></li>
<li>build(deps-dev): bump the npm group with 2 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4274">peter-evans/create-pull-request#4274</a></li>
<li>build(deps-dev): bump undici from 6.22.0 to 6.23.0 by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4284">peter-evans/create-pull-request#4284</a></li>
<li>Update distribution by <a href="https://github.com/actions-bot"><code>@actions-bot</code></a> in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4289">peter-evans/create-pull-request#4289</a></li>
<li>fix: Handle remote prune failures gracefully on self-hosted runners by <a href="https://github.com/peter-evans"><code>@peter-evans</code></a> in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4295">peter-evans/create-pull-request#4295</a></li>
<li>feat: add <code>@octokit/plugin-retry</code> to handle retriable server errors by <a href="https://github.com/peter-evans"><code>@peter-evans</code></a> in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4298">peter-evans/create-pull-request#4298</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/deining"><code>@deining</code></a> made their first contribution in <a href="https://redirect.github.com/peter-evans/create-pull-request/pull/4265">peter-evans/create-pull-request#4265</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/peter-evans/create-pull-request/compare/v8.0.0...v8.1.0">https://github.com/peter-evans/create-pull-request/compare/v8.0.0...v8.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/c0f553fe549906ede9cf27b5156039d195d2ece0"><code>c0f553f</code></a> feat: add <code>@octokit/plugin-retry</code> to handle retriable server errors (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4298">#4298</a>)</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/70001242bfa9ec7844891e620fdda69a2a2a06c7"><code>7000124</code></a> fix: Handle remote prune failures gracefully (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4295">#4295</a>)</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/34aa40e9cf0bb8b5be745a552003fdeb25e4dd3a"><code>34aa40e</code></a> build: update distribution (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4289">#4289</a>)</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/641099ddca097df58c3369dd5e1f33322b223029"><code>641099d</code></a> build(deps-dev): bump undici from 6.22.0 to 6.23.0 (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4284">#4284</a>)</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/2271f1ddcf09437ed8f019733eb5cfba58ac76f0"><code>2271f1d</code></a> build(deps-dev): bump the npm group with 2 updates (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4274">#4274</a>)</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/437c31a11dd02128dd37633ad8d3832853477e7a"><code>437c31a</code></a> build(deps): bump the github-actions group with 2 updates (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4273">#4273</a>)</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/0979079bc20c05bbbb590a56c21c4e2b1d1f1bbe"><code>0979079</code></a> docs: update readme</li>
<li><a href="https://github.com/peter-evans/create-pull-request/commit/5b751cdf403b4f0314c656b2618939e4c8bdf824"><code>5b751cd</code></a> README.md: bump given GitHub actions to their latest versions (<a href="https://redirect.github.com/peter-evans/create-pull-request/issues/4265">#4265</a>)</li>
<li>See full diff in <a href="https://github.com/peter-evans/create-pull-request/compare/98357b18bf14b5342f975ff684046ec3b2a07725...c0f553fe549906ede9cf27b5156039d195d2ece0">compare view</a></li>
</ul>
</details>
<br />
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/upload-artifact/releases">actions/upload-artifact's releases</a>.</em></p>
<blockquote>
<h2>v7.0.0</h2>
<h2>v7 What's new</h2>
<h3>Direct Uploads</h3>
<p>Adds support for uploading single files directly (unzipped). Callers can set the new <code>archive</code> parameter to <code>false</code> to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The <code>name</code> parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.</p>
<h3>ESM</h3>
<p>To support new versions of the <code>@actions/*</code> packages, we've upgraded the package to ESM.</p>
<h2>What's Changed</h2>
<ul>
<li>Add proxy integration test by <a href="https://github.com/Link"><code>@Link</code></a>- in <a href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
<li>Upgrade the module to ESM and bump dependencies by <a href="https://github.com/danwkennedy"><code>@danwkennedy</code></a> in <a href="https://redirect.github.com/actions/upload-artifact/pull/762">actions/upload-artifact#762</a></li>
<li>Support direct file uploads by <a href="https://github.com/danwkennedy"><code>@danwkennedy</code></a> in <a href="https://redirect.github.com/actions/upload-artifact/pull/764">actions/upload-artifact#764</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/Link"><code>@Link</code></a>- made their first contribution in <a href="https://redirect.github.com/actions/upload-artifact/pull/754">actions/upload-artifact#754</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/upload-artifact/compare/v6...v7.0.0">https://github.com/actions/upload-artifact/compare/v6...v7.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/upload-artifact/commit/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"><code>bbbca2d</code></a> Support direct file uploads (<a href="https://redirect.github.com/actions/upload-artifact/issues/764">#764</a>)</li>
<li><a href="https://github.com/actions/upload-artifact/commit/589182c5a4cec8920b8c1bce3e2fab1c97a02296"><code>589182c</code></a> Upgrade the module to ESM and bump dependencies (<a href="https://redirect.github.com/actions/upload-artifact/issues/762">#762</a>)</li>
<li><a href="https://github.com/actions/upload-artifact/commit/47309c993abb98030a35d55ef7ff34b7fa1074b5"><code>47309c9</code></a> Merge pull request <a href="https://redirect.github.com/actions/upload-artifact/issues/754">#754</a> from actions/Link-/add-proxy-integration-tests</li>
<li><a href="https://github.com/actions/upload-artifact/commit/02a8460834e70dab0ce194c64360c59dc1475ef0"><code>02a8460</code></a> Add proxy integration test</li>
<li>See full diff in <a href="https://github.com/actions/upload-artifact/compare/v6...v7">compare view</a></li>
</ul>
</details>
<br />
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
This PR adds new CI job `check-docs` to validate that documentation builds for all workspace packages, also adds a new justfile recipe `just doc`.
It also fixes the existing errors in `bdk_chain` and `bdk_core`.
### Changelog notice
```
### Added
- ci: add new `check-docs` job.
### Changed
- fix(docs): in `keychain_txout.rs` and `spk_client.rs`
```
### 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
* [x] 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
* [x] I'm linking the issue being fixed by this PR
Previously `fetch_txs_with_outpoints` collected spend txids into a Vec, so the same txid could be pushed multiple times when one transaction spent several input outpoints. This caused redundant `get_tx_info` calls to Esplora for the same transaction, wasting network and CPU without changing the resulting `TxUpdate`.
Use `HashSet<Txid>` for `missing_txs` in both async and blocking `fetch_txs_with_outpoints,` so each txid is only requested once while keeping the observable behaviour of `SyncResponse` / `TxUpdate` unchanged.
### Changelog notice
```
### Changed
- Use `HashSet` instead of `Vec` to track `missing_txs` in bdk_esplora, it deduplicates txids.
```
### 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
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2 to 3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/create-github-app-token/releases">actions/create-github-app-token's releases</a>.</em></p>
<blockquote>
<h2>v3.0.0</h2>
<h1><a href="https://github.com/actions/create-github-app-token/compare/v2.2.2...v3.0.0">3.0.0</a> (2026-03-14)</h1>
<ul>
<li>feat!: node 24 support (<a href="https://redirect.github.com/actions/create-github-app-token/issues/275">#275</a>) (<a href="https://github.com/actions/create-github-app-token/commit/2e564a0bb8e7cc2b907b2401a2afe177882d4325">2e564a0</a>)</li>
<li>fix!: require <code>NODE_USE_ENV_PROXY</code> for proxy support (<a href="https://redirect.github.com/actions/create-github-app-token/issues/342">#342</a>) (<a href="https://github.com/actions/create-github-app-token/commit/4451bcbc139f8124b0bf04f968ea2586b17df458">4451bcb</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>remove custom proxy handling (<a href="https://redirect.github.com/actions/create-github-app-token/issues/143">#143</a>) (<a href="https://github.com/actions/create-github-app-token/commit/dce0ab05f36f30b22fd14289fd36655c618e4e8e">dce0ab0</a>)</li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>Custom proxy handling has been removed. If you use HTTP_PROXY or HTTPS_PROXY, you must now also set NODE_USE_ENV_PROXY=1 on the action step.</li>
<li>Requires <a href="https://github.com/actions/runner/releases/tag/v2.327.1">Actions Runner v2.327.1</a> or later if you are using a self-hosted runner.</li>
</ul>
<h2>v3.0.0-beta.6</h2>
<h1><a href="https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.5...v3.0.0-beta.6">3.0.0-beta.6</a> (2026-03-13)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> bump <code>@actions/core</code> from 1.11.1 to 3.0.0 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/337">#337</a>) (<a href="https://github.com/actions/create-github-app-token/commit/b04413352d4644ac2131b9a90c074f5e93ca18a1">b044133</a>)</li>
<li><strong>deps:</strong> bump minimatch from 9.0.5 to 9.0.9 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/335">#335</a>) (<a href="https://github.com/actions/create-github-app-token/commit/5cbc65624c9ddc4589492bda7c8b146223e8c3e4">5cbc656</a>)</li>
<li><strong>deps:</strong> bump the production-dependencies group with 4 updates (<a href="https://redirect.github.com/actions/create-github-app-token/issues/336">#336</a>) (<a href="https://github.com/actions/create-github-app-token/commit/6bda5bc1410576b9a0879ce6076d53345485bba9">6bda5bc</a>)</li>
<li><strong>deps:</strong> bump undici from 7.16.0 to 7.18.2 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/323">#323</a>) (<a href="https://github.com/actions/create-github-app-token/commit/b4f638f48ee0dcdbb0bc646c48e4cb2a2de847fe">b4f638f</a>)</li>
</ul>
<h2>v3.0.0-beta.5</h2>
<h1><a href="https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.4...v3.0.0-beta.5">3.0.0-beta.5</a> (2026-03-13)</h1>
<ul>
<li>fix!: require <code>NODE_USE_ENV_PROXY</code> for proxy support (<a href="https://redirect.github.com/actions/create-github-app-token/issues/342">#342</a>) (<a href="https://github.com/actions/create-github-app-token/commit/d53a1cdfde844c958786293adcaf739ecb8b5eb9">d53a1cd</a>)</li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>Custom proxy handling has been removed. If you use HTTP_PROXY or HTTPS_PROXY, you must now also set NODE_USE_ENV_PROXY=1 on the action step.</li>
</ul>
<h2>v3.0.0-beta.4</h2>
<h1><a href="https://github.com/actions/create-github-app-token/compare/v3.0.0-beta.3...v3.0.0-beta.4">3.0.0-beta.4</a> (2026-03-13)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> bump <code>@octokit/auth-app</code> from 7.2.1 to 8.0.1 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/257">#257</a>) (<a href="https://github.com/actions/create-github-app-token/commit/bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1">bef1eaf</a>)</li>
<li><strong>deps:</strong> bump <code>@octokit/request</code> from 9.2.3 to 10.0.2 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/256">#256</a>) (<a href="https://github.com/actions/create-github-app-token/commit/5d7307be63501c0070c634b0ae8fec74e8208130">5d7307b</a>)</li>
<li><strong>deps:</strong> bump glob from 10.4.5 to 10.5.0 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/305">#305</a>) (<a href="https://github.com/actions/create-github-app-token/commit/5480f4325a18c025ee16d7e081413854624e9edc">5480f43</a>)</li>
<li><strong>deps:</strong> bump p-retry from 6.2.1 to 7.1.0 (<a href="https://redirect.github.com/actions/create-github-app-token/issues/294">#294</a>) (<a href="https://github.com/actions/create-github-app-token/commit/dce3be8b284f45e65caed11a610e2bef738d15b4">dce3be8</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/create-github-app-token/commit/f8d387b68d61c58ab83c6c016672934102569859"><code>f8d387b</code></a> build(release): 3.0.0 [skip ci]</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/d2129bd463d4feb8723edeea9437baa7db58e41e"><code>d2129bd</code></a> style: remove extra blank line in release workflow</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/77b94efc3e5f99a45abdd163fe04a4ebb95e98d6"><code>77b94ef</code></a> build: refresh generated artifacts</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/3ab4c6689898955f913a485593b36b197c6dbbdc"><code>3ab4c66</code></a> chore: move undici to devDependencies</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/739cf66feb937a443e4b6b7626bedd98f9fef6df"><code>739cf66</code></a> docs: update README action versions</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/db40289976a36527816d4f6f45765fdee71f134b"><code>db40289</code></a> build(deps): bump actions versions in test.yml</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/496a7ac4eb472eeac44d67818d1ce7f5e9e5fc97"><code>496a7ac</code></a> test: migrate from AVA to Node.js native test runner (<a href="https://redirect.github.com/actions/create-github-app-token/issues/346">#346</a>)</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/3870dc3051e3f1fc3a2faa17bcbb00f31fe1dd6c"><code>3870dc3</code></a> Rename end-to-end proxy job in test workflow</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/4451bcbc139f8124b0bf04f968ea2586b17df458"><code>4451bcb</code></a> fix!: require <code>NODE_USE_ENV_PROXY</code> for proxy support (<a href="https://redirect.github.com/actions/create-github-app-token/issues/342">#342</a>)</li>
<li><a href="https://github.com/actions/create-github-app-token/commit/dce0ab05f36f30b22fd14289fd36655c618e4e8e"><code>dce0ab0</code></a> fix: remove custom proxy handling (<a href="https://redirect.github.com/actions/create-github-app-token/issues/143">#143</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/actions/create-github-app-token/compare/v2...v3">compare view</a></li>
</ul>
</details>
<br />
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Bumps [Swatinem/rust-cache](https://github.com/swatinem/rust-cache) from 2.8.2 to 2.9.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/swatinem/rust-cache/releases">Swatinem/rust-cache's releases</a>.</em></p>
<blockquote>
<h2>v2.9.1</h2>
<p>Fix regression in hash calculation</p>
<p><strong>Full Changelog</strong>: <a href="https://github.com/Swatinem/rust-cache/compare/v2.9.0...v2.9.1">https://github.com/Swatinem/rust-cache/compare/v2.9.0...v2.9.1</a></p>
<h2>v2.9.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Add support for running rust-cache commands from within a Nix shell by <a href="https://github.com/marc0246"><code>@marc0246</code></a> in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/290">Swatinem/rust-cache#290</a></li>
<li>Bump taiki-e/install-action from 2.62.57 to 2.62.60 in the actions group by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/291">Swatinem/rust-cache#291</a></li>
<li>Bump the actions group across 1 directory with 5 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/296">Swatinem/rust-cache#296</a></li>
<li>Bump the prd-major group with 3 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/294">Swatinem/rust-cache#294</a></li>
<li>Bump <code>@types/node</code> from 24.10.1 to 25.0.2 in the dev-major group by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/295">Swatinem/rust-cache#295</a></li>
<li>Consider all installed toolchains in cache key by <a href="https://github.com/tamird"><code>@tamird</code></a> in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/293">Swatinem/rust-cache#293</a></li>
<li>Compare case-insenitively for full cache key match by <a href="https://github.com/kbriggs"><code>@kbriggs</code></a> in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/303">Swatinem/rust-cache#303</a></li>
<li>Migrate to <code>node24</code> runner by <a href="https://github.com/rhysd"><code>@rhysd</code></a> in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/314">Swatinem/rust-cache#314</a></li>
<li>Bump the actions group across 1 directory with 7 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/312">Swatinem/rust-cache#312</a></li>
<li>Bump the prd-minor group across 1 directory with 2 updates by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/307">Swatinem/rust-cache#307</a></li>
<li>Bump <code>@types/node</code> from 25.0.2 to 25.2.2 in the dev-minor group by <a href="https://github.com/dependabot"><code>@dependabot</code></a>[bot] in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/309">Swatinem/rust-cache#309</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/marc0246"><code>@marc0246</code></a> made their first contribution in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/290">Swatinem/rust-cache#290</a></li>
<li><a href="https://github.com/tamird"><code>@tamird</code></a> made their first contribution in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/293">Swatinem/rust-cache#293</a></li>
<li><a href="https://github.com/kbriggs"><code>@kbriggs</code></a> made their first contribution in <a href="https://redirect.github.com/Swatinem/rust-cache/pull/303">Swatinem/rust-cache#303</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/Swatinem/rust-cache/compare/v2.8.2...v2.9.0">https://github.com/Swatinem/rust-cache/compare/v2.8.2...v2.9.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/Swatinem/rust-cache/blob/master/CHANGELOG.md">Swatinem/rust-cache's changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>2.9.1</h2>
<ul>
<li>Fix regression in hash calculation</li>
</ul>
<h2>2.9.0</h2>
<ul>
<li>Update to <code>node24</code></li>
<li>Support running from within a <code>nix</code> shell</li>
<li>Consider all installed toolchains for cache key</li>
<li>Use case-insensitive comparison to determine exact cache hit</li>
</ul>
<h2>2.8.2</h2>
<ul>
<li>Don't overwrite env for cargo-metadata call</li>
</ul>
<h2>2.8.1</h2>
<ul>
<li>Set empty <code>CARGO_ENCODED_RUSTFLAGS</code> when retrieving metadata</li>
<li>Various dependency updates</li>
</ul>
<h2>2.8.0</h2>
<ul>
<li>Add support for <code>warpbuild</code> cache provider</li>
<li>Add new <code>cache-workspace-crates</code> feature</li>
</ul>
<h2>2.7.8</h2>
<ul>
<li>Include CPU arch in the cache key</li>
</ul>
<h2>2.7.7</h2>
<ul>
<li>Also cache <code>cargo install</code> metadata</li>
</ul>
<h2>2.7.6</h2>
<ul>
<li>Allow opting out of caching $CARGO_HOME/bin</li>
<li>Add runner OS in cache key</li>
<li>Adds an option to do lookup-only of the cache</li>
</ul>
<h2>2.7.5</h2>
<ul>
<li>Support Cargo.lock format cargo-lock v4</li>
<li>Only run macOsWorkaround() on macOS</li>
</ul>
<h2>2.7.3</h2>
<ul>
<li>Work around upstream problem that causes cache saving to hang for minutes.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/Swatinem/rust-cache/commit/c19371144df3bb44fab255c43d04cbc2ab54d1c4"><code>c193711</code></a> 2.9.1</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/781e8d91ab29deb65464798965e49853f963b561"><code>781e8d9</code></a> try reverting pipeline change</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/3d1fa4654a5786f5537b1d31acd0f35e56de9924"><code>3d1fa46</code></a> add changelog</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/c676846f29d98ff6b0106d3608c7ffd4048af17b"><code>c676846</code></a> 2.9.0</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/bf71d02c11df9d5253618f39943e9dd59f7fd5a9"><code>bf71d02</code></a> bump dependencies and rebuild</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/8a02ed5e290d8afc7e587930243f3016b3223f50"><code>8a02ed5</code></a> Bump <code>@types/node</code> from 25.0.2 to 25.2.2 in the dev-minor group (<a href="https://redirect.github.com/swatinem/rust-cache/issues/309">#309</a>)</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/390157d4874246aff722dd7f77e641fcae197678"><code>390157d</code></a> Bump the prd-minor group across 1 directory with 2 updates (<a href="https://redirect.github.com/swatinem/rust-cache/issues/307">#307</a>)</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/68500c182e89a3f56d9b1de095d7e62f0ea5b8bf"><code>68500c1</code></a> Bump the actions group across 1 directory with 7 updates (<a href="https://redirect.github.com/swatinem/rust-cache/issues/312">#312</a>)</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/1a8384176d7ed15c323a201c65073983cdb5a5be"><code>1a83841</code></a> Migrate to <code>node24</code> runner (<a href="https://redirect.github.com/swatinem/rust-cache/issues/314">#314</a>)</li>
<li><a href="https://github.com/Swatinem/rust-cache/commit/11da8522bc3856a8fbc565f1d1530989c793d67d"><code>11da852</code></a> Compare case-insenitively for full cache key match (<a href="https://redirect.github.com/swatinem/rust-cache/issues/303">#303</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/swatinem/rust-cache/compare/779680da715d629ac1d338a641029a2f4372abb5...c19371144df3bb44fab255c43d04cbc2ab54d1c4">compare view</a></li>
</ul>
</details>
<br />
Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
This introduces clear temporal context **documentation** to the `TxUpdate` struct, explicitly stating that entries must have either `anchors` or `seen_ats` to be considered canonical and contribute to wallet balances.
It also explicitly documents the `seen_ats` HashSet signature to prevent usage errors when writing custom chain sources.
This fulfills the recommendation outlined in the Wizardsardine BDK Audit Report (Q4 2024).
### Description
This primarily affects developers building **custom chain sources** — anyone constructing `Update` structs outside of `bdk_electrum`/`bdk_esplora`/`bdk_bitcoind_rpc`.
As the ecosystem grows (streaming Electrum, Nostr relay sync, compact block filters, custom backends), more developers will encounter this undocumented contract.
### What I Changed
*1. Doc comment on `TxUpdate` struct*
* **`anchors` or `seen_ats`** are stored in the graph but do not affect the balance.
* **`seen_ats` collection type**: `TxUpdate::seen_ats` is a `BTreeSet<(Txid, u64)>`, requiring `.insert((txid, timestamp))`.
*2. Doc comment on `Wallet::apply_update()`*
* **TxGraph `apply_update`**: transactions without temporal context note.
* **IndexedTxGraph `apply_update`**: transactions without temporal context note.
### Impact
This is just a documentation fix - no code changes, no breaking changes.
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
This introduces clear temporal context documentation to the `TxUpdate`
struct, explicitly stating that entries must have either `anchors` or
`seen_ats` to be considered canonical and contribute to wallet balances.
This fulfills the recommendation outlined in the Wizardsardine
BDK Audit Report (Q4 2024).
Signed-off-by: Rafael Turon <3598269+rafaelturon@users.noreply.github.com>
The blanket `Anchor` impl for `&A` was missing the `confirmation_height_upper_bound` method, causing it to fall back to the default implementation instead of delegating to the inner type.
### Changelog notice
```md
Fixed:
- The `Anchor::confirmation_height_upper_bound` impl was missing for `&A`, causing it to fallback to the default impl.
```
### 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~~
~~* [ ] I'm linking the issue being fixed by this PR~~
志宇 [Sat, 14 Feb 2026 00:21:20 +0000 (00:21 +0000)]
fix(chain): forward `confirmation_height_upper_bound` in `Anchor` impl for `&A`
The blanket `Anchor` impl for `&A` was missing the
`confirmation_height_upper_bound` method, causing it to fall back to
the default implementation instead of delegating to the inner type.
I didn't realize #2053 didn't actually compile when I merged it. This PR fixes this by bumping `esplora_client` to `0.12.3` so that the `.get_block_infos` method is always available.
### Changelog
```md
Fixed:
- Bump `esplora_client` to `0.12.3` so that the `.get_block_infos` method is always available.
```
### Checklists
#### All Submissions:
* [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
Add `mine_block` to `bdk_testenv::Env` with custom timestamp and coinbase address. This allows us to test timelocked transactions.
### Changelog notice
```md
Added
- `mine_block` method to `bdk_testenv::Env` with custom `MineParams`. Blocks can be mined with no transactions, contain custom timestamp and custom coinbase address.
- `min_time_for_next_block` and `get_block_template` helper methods.
```
### 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
志宇 [Sat, 24 Jan 2026 10:20:23 +0000 (10:20 +0000)]
feat(testenv): add `mine_block` with custom timestamp and coinbase address
Refactor block mining in `TestEnv` to use `getblocktemplate` RPC properly:
- Add `MineParams` struct to configure mining (empty blocks, custom
timestamp, custom coinbase address)
- Add `mine_block()` method that builds blocks from the template with
proper BIP34 coinbase scriptSig, witness commitment, and merkle root
- Add `min_time_for_next_block()` and `get_block_template()` helpers
- Refactor `mine_empty_block()` to use the new `mine_block()` API
- Include mempool transactions when `empty: false`
<!-- You can erase any parts of this template not applicable to your Pull Request. -->
### Description
This PR addresses a panic path in stop gap scan loop. Removes `expect` used to compute gap boundary and instead tracks `consecutive_unused` with `gap_limit = stop_gap.max(1)` to decide when to stop scanning.
Spurred by trying to address https://github.com/bitcoindevkit/bdk_wallet/issues/30 for Esplora
### Notes to the reviewers
Open to any and all feedback on this.
Behavior is unchanged for typical cases, it just avoids a panic in the control flow.
### Changelog notice
```
### Fix:
- Avoid a panic in the Esplora stop‑gap scan loop by tracking consecutive unused scripts to compute the gap boundary.
```
### 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
The `get_blocks` method from `esplora-client` has been deprecated on the latest release `v0.12.3`. I'm updating it to use newly added one: `get_blocks_infos` in order to fix the deprecation warning.
### Changelog notice
```
### Changed
- fix(bdk_esplora): use `get_block_infos` instead of deprecated `get_blocks`
```
### 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
Automated update to Github CI workflow `cont_integration.yml` by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action