feat(chain)!: taint-aware CanonicalView::balance via canonical ancestor walk#2235
Draft
evanlinjin wants to merge 6 commits into
Draft
feat(chain)!: taint-aware CanonicalView::balance via canonical ancestor walk#2235evanlinjin wants to merge 6 commits into
CanonicalView::balance via canonical ancestor walk#2235evanlinjin wants to merge 6 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2235 +/- ##
==========================================
+ Coverage 78.65% 78.86% +0.20%
==========================================
Files 30 31 +1
Lines 5909 6076 +167
Branches 279 285 +6
==========================================
+ Hits 4648 4792 +144
- Misses 1185 1209 +24
+ Partials 76 75 -1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
1fb7787 to
0a9fa74
Compare
Add `Canonical::ancestors`, returning a `CanonicalAncestors` iterator that walks the canonical ancestors of a set of seed transactions. - Ancestors are yielded in reverse topological order (descendants before the ancestors they spend) and each transaction is visited exactly once. - Accumulation is a fold over the ancestor DAG: `map` computes each tx's own contribution from its `CanonicalTx`, and a tx's final accumulator is its contribution `Merge`d with the accumulators of every in-set descendant. - `should_walk` prunes the walk per-tx, with the chain position available so callers can decide cutoffs (e.g. stop at confirmed). Seeds are given as txids (looked up in the canonical set) and seed the accumulation without being yielded. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a variant of `Canonical::ancestors` that also yields the seed transactions (each with its own final accumulator), not just their ancestors. Pruning, deduplication and the reverse-topological order are unchanged; the seeds are emitted alongside the walked ancestors. Useful when the per-seed contribution must be folded uniformly with the ancestry (e.g. a seed that taints itself), avoiding a separate pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace `balance`'s `trust_predicate` and `min_confirmations` parameters with two closures: * `does_taint(CanonicalTx) -> bool` decides whether a transaction taints its descendants. A pending output is `untrusted_pending` if it, or any of its unsettled ancestors, taints; otherwise `trusted_pending`. The unsettled ancestry of each pending output is walked via `ancestors_inclusive` (deduped across outputs, stopping at settled transactions). This lets callers demote unconfirmed coins received from (or chained on top of) a third party. * `is_settled(&ChainPosition) -> bool` decides the confirmed/pending boundary, generalizing the old numeric `min_confirmations` (e.g. it can treat shallowly-confirmed outputs as pending and taintable). `does_taint` replaces the per-spk `trust_predicate`: trust is now derived from ancestry rather than a per-script heuristic. Call sites and tests are updated; the old per-keychain trust test is recomputed for the new model. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The balance "confirmed" bucket is now driven by the caller-supplied `is_settled` predicate (transactions we are confident will not be replaced), not strictly by confirmation status. Rename the field to match the concept and update `Display`, `Add`, `total`, `trusted_spendable` and all call sites. This is a breaking change: the serde key changes from "confirmed" to "settled" (no alias). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0a9fa74 to
7737e30
Compare
2 tasks
Add `CanonicalView::classify_outpoints`, which pairs each unspent output with a chain-level `Eligibility` (`Settled` / `Immature` / `TrustedPending` / `UntrustedPending`) computed from `is_settled` and the `does_taint` ancestry walk. This is the primitive for coin selection / coin control: the caller decides how to aggregate, and can layer wallet-specific categories (e.g. "locked") on top — rather than being constrained to the fixed `Balance` buckets. `CanonicalView::balance` is re-expressed as a thin fold over `classify_outpoints`, adding each output's value to the `Balance` bucket that matches its `Eligibility`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
a286ab4 to
ab0c5a2
Compare
… walk Close review-identified coverage gaps: - `test_balance_taint_stops_at_settled_ancestor`: a settled ancestor that `does_taint` would flag must not taint its unsettled descendant — isolates the `!is_settled` guard in the taint walk. - `test_classify_immature_and_settled`: `classify_outpoints` returns `Immature` for an immature coinbase and `Settled` for a mature output. - `ancestors_inclusive_yields_seeds`: the inclusive walk yields the seeds (with their own accumulators) and `len()` accounts for them. - `ancestors_multiple_seeds_dedup_shared_ancestor`: an ancestor shared by two seeds is visited once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
c9c6f24 to
71c35d9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Warning
This PR — including this description — is currently entirely AI-generated.
It still needs proper human review (by me, @evanlinjin) before it should be taken seriously. Treat everything below as a draft proposal, not a vetted design.
Description
This is an alternative direction to #2221. Where #2221 makes
CanonicalView::balanceless restrictive by removing themin_confirmationsparameter, this PR instead tries to make it more useful — handing control to the caller via closures, deriving trust from ancestry rather than a per-script heuristic, and exposing a per-output spend-eligibility classifier that's the building block for coin control.The core new primitive is
CanonicalView::classify_outpoints, built on a reusable, sans-TxGraphcanonical ancestor walk.balancebecomes a thin fold over it.What's here (commit by commit)
1. Add
CanonicalAncestorsreverse-topological walk.Canonical::ancestors(seeds, map, should_walk)walks the canonical ancestors of a set of seed txids backwards (the seeds are the leaf-most txs, closer to the leaves of the ancestry DAG than its roots). Each tx is visited once in reverse-topological order, folding aMergeable accumulator from descendants into ancestors (diamonds merge correctly).should_walkprunes per-tx with theChainPositionavailable; the iterator isExactSizeIterator.2. Add
Canonical::ancestors_inclusive. A variant that also yields the seeds (each with its own final accumulator), so a per-seed contribution can be folded uniformly with the ancestry.3. Taint-aware
CanonicalView::balance(breaking). Replacestrust_predicate+min_confirmationswith two closures:does_taint(CanonicalTx) -> bool: a pending output is untrusted if it, or any of its unsettled ancestors, taints. The unsettled ancestry is walked once (deduped across outputs, stopping at settled txs) viaancestors_inclusive. This lets callers demote unconfirmed coins received from — or chained on top of — a third party. Trust is now derived from ancestry instead of a per-script heuristic.is_settled(&ChainPosition) -> bool: generalizes the numericmin_confirmationsinto a caller-defined "confident this won't be replaced" boundary, and is the sole authority on it — a settled output is never dropped or tainted (even an unconfirmed output a caller chooses to treat as settled is counted, not lost).balancealso drops itsOidentifier generic and now takes bareOutPoints.4. Rename
Balance::confirmedtoBalance::settled(breaking). The bucket is driven byis_settled(confidence a tx won't be replaced), not strictly confirmation status, so the field is renamed to match. Breaking serde key change (confirmed→settled, no alias).5. Add
classify_outpointsspend-eligibility classifier.CanonicalView::classify_outpointspairs each unspent output with a chain-levelEligibility(Settled/Immature/TrustedPending/UntrustedPending) fromis_settled+ thedoes_taintwalk. This is the primitive for coin selection / coin control (e.g. "prefer settled coins, fall back to trusted-pending"): the caller decides how to aggregate and can layer wallet-specific categories like "locked" on top, instead of being constrained to the fixedBalancebuckets.balanceis re-expressed as a thin fold over it.Direction / open questions for human review
classify_outpointsis the real API andBalanceis just one possible fold — wallets that want categories like "locked"/"reserved" aggregate themselves. This PR keepsBalance(as a fold) for now; a follow-up could move it down intobdk_walletand drop it frombdk_chain.trust_predicatewith ancestry-baseddoes_taintthe right call, or should both coexist? (does_taintcan't express per-output trust, but per-tx ancestry is arguably the more principled model.)Balance::confirmed→Balance::settled: worth the rename / serde break, or keep the conventional name?bdk_walletwill need itsbalancecall updated (out of this repo's tree).Notes to reviewers
classify_outpointsancestry-taint test, and a regression test that a settled-everythingis_settlednever drops an unconfirmed output. Existing balance tests were migrated (the old per-keychain trust test is recomputed for the new model, since none of its txs actually spend third-party coins).cargo fmt,clippy --all-features --all-targets -p bdk_chain, andRUSTDOCFLAGS="-D warnings" cargo docare clean; thebdk_chainsuite + doctests pass.Changelog notice
Changed
CanonicalView::balancenow takesdoes_taintandis_settledclosures instead oftrust_predicateandmin_confirmations, and accepts bareOutPoints.Balance::confirmedtoBalance::settled.Added
CanonicalView::classify_outpoints+Eligibility: per-output spend-eligibility classification (the building block for coin control).Canonical::ancestors/Canonical::ancestors_inclusive: reverse-topological canonical ancestor walk with aMergeable accumulator.🤖 Generated with Claude Code