feat: add bdk_wallet_tx bridge crate (workspace)#84
Open
evanlinjin wants to merge 24 commits into
Open
Conversation
`get_key` decided whether a key origin covered a `KeyRequest::Bip32` derivation by stringifying both paths and using `starts_with`. Since `DerivationPath` renders components as `/`-joined decimals, this matched on character boundaries rather than components: `m/1` spuriously matched `m/10`, and an unhardened origin matched its hardened sibling. The signer could then return a private key for a derivation the descriptor never meant to expose. Delegate the check to miniscript's `DescriptorXKey::matches`, which confirms the key actually represents the request (handling the key origin and wildcard), then strip the origin prefix and derive the remainder. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously, we had a string constants for test descriptors and test keys where the test keys were also hardcoded in the test descriptors. We remove the hard coded test descriptor and rename variables for clarity.
Any Plan whose Schnorr witness template includes a 64B signature gets a DEFAULT sighash type (this includes mixed-size plans). All other plans require ALL sighash.
**BIP174:** All other data except the UTXO and unknown fields (including PSBT_IN_PROPRIETARY fields the Input Finalizer does not understand) in the input key-value map should be cleared from the PSBT.
Finalization can now fail for reasons miniscript does not model, so `finalize_input` and `FinalizeMap` return a dedicated `FinalizeError` instead of `miniscript::Error`. The finalizer now rejects an input when: - a signature's sighash type disagrees with the declared `PSBT_IN_SIGHASH_TYPE` (mandated by BIP174); - no type is declared yet a signature is neither DEFAULT nor ALL; - a satisfied schnorr witness is larger than the plan committed to (e.g. a 65-byte SIGHASH_ALL sig where 64-byte DEFAULT was planned), which would make the transaction undershoot its target feerate and risk being unbroadcastable. BREAKING CHANGE: `Finalizer::finalize_input`, `FinalizeMap`, and `FinalizeMap::results` now use `FinalizeError` in place of `miniscript::Error`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add tests for the two sighash checks `finalize_input` now performs: `SighashMismatch` (declared PSBT_IN_SIGHASH_TYPE disagrees with the signature) and `SighashNotAllowed` (no type declared, signature is neither DEFAULT nor ALL). `SignatureTooLarge` is left uncovered: it is only reachable in release builds, since in debug miniscript's `satisfy_self` panics on a `debug_assert!` of the signature size before the check is reached. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5 tasks
d378d5f to
edfd2fc
Compare
c26de18 to
7b38e3e
Compare
bitcoindevkit#74 makes selection always declare PSBT_IN_SIGHASH_TYPE, so the test must clear it to exercise the no-declaration path that yields SighashNotAllowed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure rename — same struct, same methods, same parameters. No behaviour change. The next commit adds the resolved tx-shape fields (version, lock_time, fallback_sequence), the corresponding setters, and the PSBT/AFS pipeline that consumes them. Selection -> TxTemplate Selection::new -> TxTemplate::from_parts (still pub(crate)) IntoSelectionError -> IntoTxTemplateError InputCandidates::into_selection -> into_tx_template Selector::try_finalize() -> Option<TxTemplate> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes bitcoindevkit#57. TxTemplate now owns the resolved tx-shape fields and the methods that mutate them. The selector hands you a TxTemplate already configured with sensible defaults; everything else is method calls on it. New fields on TxTemplate: - version (default V2) - lock_time (= max(input CLTV) or ZERO) - fallback_sequence (default ENABLE_RBF_NO_LOCKTIME) New setters with validation: - set_version -> SetVersionError::RelativeTimelockRequiresV2 - set_locktime -> SetLockTimeError::{BelowInputCltv, UnitMismatch} - set_fallback_sequence The PSBT/AFS pipeline is restructured around these fields: - PsbtParams -> PsbtBuildParams (PSBT-only knobs; version/locktime /AFS removed) - CreatePsbtError -> BuildPsbtError - create_psbt(params) -> (Psbt, Finalizer) (was just Psbt) - anti-fee-sniping moves off PsbtParams::anti_fee_sniping into TxTemplate::apply_anti_fee_sniping(tip, &mut rng), a separate chainable step that composes the public set_locktime / Input::set_sequence - to_unsigned_tx() materializes the tx for non-PSBT signing flows Chain ergonomics: sort_inputs_by / shuffle_inputs (etc.) now consume self and return Self. into_finalizer is dropped — Finalizer comes from create_psbt or from Finalizer::new for callers that want it standalone. What was previously silent is now an explicit error: - min_locktime of the wrong unit was silently ignored - min_locktime below an input's CLTV was silently clamped up Both now error via SetLockTimeError. Setting v < 2 with a relative- timelock input errors via SetVersionError. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
create_psbt no longer needs an RNG (AFS — the only consumer — takes its own rng explicitly), so the create_psbt_with_rng wrapper and its thread_rng() call were dead weight. Collapses both into a single create_psbt(self, params) and moves rand to dev-dependencies. The library now depends only on rand_core (for the RngCore trait) + miniscript + bdk_coin_select. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settle on the "build" verb so the method, params, and error type agree: create_psbt -> build_psbt, PsbtBuildParams -> BuildPsbtParams (also fixing the word order). Move BuildPsbtParams/BuildPsbtError into a new build_psbt module; the build_psbt method stays inherent on TxTemplate since it touches private fields. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fee-sniping apply_anti_fee_sniping now consumes the template and returns a SealedTxTemplate exposing only reads + emission, so version/locktime/sequence/ordering can't be changed after AFS. TxTemplate wraps SealedTxTemplate and derefs to it for the shared read/emit surface; the free afs helper mutates &mut self in place and the method does the sealing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Provide selection_algorithm_single_random_draw, which shuffles candidates with a caller-supplied rng and selects until the target is met. Pass it to Selector::select_with_algorithm so randomness lives at the selection step and candidate construction stays deterministic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Incremental builders to add an Input to the must-select group or as an optional can-select group, with outpoint de-duplication. Lets callers extend an InputCandidates after construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously these were no-ops when the outpoint already existed, so a push could neither promote a can-select candidate to must-select nor replace stale input data, and the surviving candidate depended on insertion order rather than group precedence. They now upsert: the existing candidate is replaced and moved to the requested group, with must-select taking precedence over can-select (consistent with `new`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…te/build_psbt API The sighash tests added by the finalizer/selection PRs were authored against the pre-TxTemplate API; update them to TxTemplate::new + build_psbt (which now returns (Psbt, Finalizer)) and the merged test-key const names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fold the whole selection lifecycle into `InputCandidates::into_selection`
and remove the public `Selector` struct. Algorithms now operate directly
on a `bdk_coin_select::CoinSelector` plus a pure-data `SelectionContext`,
instead of a wrapper that conflated lifecycle plumbing with the algorithm
handle.
- `SelectorParams`/`SelectorError` -> `SelectionParams`/`SelectionError`.
- Unify the long-term feerate into one `SelectionParams::longterm_feerate`
(`None` = target feerate), feeding both the change policy and the bnb
metric; the change policy is now always waste-aware.
- `IntoSelectionError` carries distinct `CannotMeetTarget { missing }`
(impossible) vs `AlgorithmFellShort` (algorithm under-selected) variants.
- Fix the reachability pre-check to select effective inputs at the resolved
target feerate (`target.fee.rate`), not the raw one, avoiding a false
`CannotMeetTarget` under RBF.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…match` `SelectionError` lumped change-policy construction failures together with candidate timelock validation, so `to_cs_change_policy` advertised a `LockTypeMismatch` variant it could never return. - `SelectionError` -> `ChangePolicyError` (just `Miniscript` / `InsufficientAssets`), now the honest return type of `to_cs_change_policy`. - `IntoSelectionError::Setup(_)` -> `ChangePolicy(ChangePolicyError)`, and `LockTypeMismatch` is promoted to a top-level `IntoSelectionError` variant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The module no longer holds a `Selector`; it now holds `SelectionParams` and its supporting config/context/error types. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the bdk_tx crate under tx/ and add a workspace root so a sibling bdk_wallet_tx crate can live under wallet_tx/. CI feature flags are scoped to -p bdk_tx (no_std) and --workspace (all-features). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2872626 to
6dae003
Compare
Bridge between bdk_wallet and bdk_tx, exposed as a `WalletTxExt` extension trait on `bdk_wallet::Wallet`. Keeping it in its own crate means neither base crate depends on the other: `bdk_wallet` stays stable, `bdk_tx` stays free to move, and the bridge absorbs the coupling (the decoupling argument from bitcoindevkit/bdk_wallet#297). Three-stage pipeline: - `candidates`/`candidates_with`/`rbf_candidates` -> `CandidateSet` (stage 1). - `select` -> `(TxTemplate, Option<AddressInfo>)` (stage 2): a pure read over a borrowed `&CandidateSet` (re-runnable) that peeks the auto-derived change address; commit it with `commit_change` (reveal + mark used) when you use the tx, or ignore it on abandon. The caller supplies the RNG. Emit the PSBT directly via `bdk_tx::TxTemplate::build_psbt`. - `add_global_xpubs` fills the PSBT's global xpubs (the only emission step that needs the wallet). Ported from the bdk_wallet#502 PoC, re-expressed over the integrated bdk_tx API and the wallet's public accessors. MTP is faked from the confirmation block time; UTXO locking is honored; `longterm_feerate` lives on `SelectParams` and feeds the waste-aware change policy for every strategy. End-to-end tests cover the pipeline, candidate filtering, change commit, and error paths. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6dae003 to
59a29ff
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.
Description
Adds
bdk_wallet_tx, a bridge crate that drivesbdk_tx's multi-stage transaction building from abdk_wallet::Wallet, and converts this repo into a Cargo workspace to house it (tx/=bdk_tx,wallet_tx/=bdk_wallet_tx).This is an alternative to embedding
bdk_txinbdk_wallet(bitcoindevkit/bdk_wallet#297, #502). Puttingbdk_tx's pre-stable types inbdk_wallet's public API would couple the stable crate to a fast-moving one — every breakingbdk_txrelease would force abdk_walletmajor. A separate crate depending on both means neither base crate depends on the other; the bridge absorbs the coupling and is versioned on its own. (Rationale: bitcoindevkit/bdk_wallet#297 comment.)WalletTxExt for bdk_wallet::Walletexposes a three-stage pipeline (ported from the bdk_wallet#502 PoC, re-expressed over the integratedbdk_txAPI and the wallet's public accessors):Notes to the reviewers
master), so most of the diff is their commits. The load-bearing dependency is theTxTemplatestage (#73) — without the multi-stage pipeline there's nothing to bridge; #82 reshapes the selection entry point this crate calls, and #74/#79/#81 are independentbdk_tximprovements bundled here for the integrated state. They don't all need to land in lockstep — once theTxTemplateline is in, this rebases down to just the workspace + bridge commits.The commits worth reviewing here — everything else is a cherry-pick (review those in their own PRs):
59a29fffeat: add bdk_wallet_tx bridge crate— the crux of this PR0010fe4e97ef02,df09bd3TxTemplate/build_psbtAPIb5eca4erustfmtafter the #73 / #82 mergeOne caveat: the #82 commits as they appear here (
4c0262d,2de1dce,3d49606) carry conflict resolution against #73 — the merged entry point isInputCandidates::into_tx_template(algorithm, SelectionParams) -> Result<TxTemplate, IntoTxTemplateError>(#73's tx-template staging on #82's single-pass internals,Selectorremoved). If you care about that reconciliation, diff them against pristine #82.Change-address lifecycle (a deliberate departure from
TxBuilder)bdk_wallet'sTxBuilder::finish()revealed and staged the change address implicitly, as a side effect of building the PSBT — building a transaction always mutated wallet state, whether or not you went on to broadcast it. Here it is explicit and caller-driven:selectonly peeks the change address (a pure&selfread returning the changeAddressInfoalongside the template, orNonewhen caller-supplied / no change output), and youreserve_changeit (=reveal_addresses_to+mark_used) to claim a distinct change address — reversible withunmark_usedif that selection ends up unused. Benefits:selectmutates nothing, so it is re-runnable (over a borrowed&CandidateSet) and safe to call freely; the only changeset to persist comes fromreserve_change, so persistence is deliberate rather than "after every build".reserve_changebundles reveal andmark_used, so reserving a selection guarantees the nextselectdraws a fresh change address. Batching several txs before broadcasting is therefore correct by construction — no address reuse — and you choose exactly which selections to reserve. (Excluding an already-built tx's inputs from the next candidate set is justcoins.filter(..), sinceselectborrowscoins.) The returnedAddressInfo.keychainis pre-mapped, so it's safe formark_used/unmark_used(which don't map it) on single-descriptor wallets — an upstream bug fixed by bitcoindevkit/bdk_wallet#507; once that lands the pre-mapping workaround here can be dropped.The trade-off is explicitness: you must
reserve_change(and persist) the selections you might broadcast, andunmark_usedany you drop —finish()revealed automatically. That's the intended shift: from automatic but always-mutating to explicit but controllable.Other design notes for the bridge (
59a29ff):TxTemplate::build_psbtneeds nothing from the wallet, so callers invoke it directly; the only wallet-specific emission step, filling global xpubs, isadd_global_xpubs(&mut Psbt).CandidateParams). Auto-gathered candidates are filtered after planning, viaInputCandidates::filter(so manually-selectedmust_spendinputs are never dropped): coins locked throughWallet::lock_outpoint, already-spent outputs, and — gated byallow_immature/allow_timelocked(both defaultfalse) — immature coinbase and not-yet-spendable CLTV/CSV outputs are excluded. Height-based timelocks resolve exactly; time-based locks need median-time-past, whichbdk_walletdoesn't retain, so the caller supplies it:tip_mtp(a single value — absolute CLTV-time and the tip side of relative locks) andfetch_mtp: Option<Box<MtpOracle>>(a per-inputFn(BlockId) -> Option<Time>oracle filling each input'sprev_mtp— relative CSV-time locks). Without them, time-based-locked inputs stay unresolved and are conservatively excluded. Unlike #502 we never fabricate MTP (prev_mtpisNoneabsent an oracle); the eventual clean fix is MTP retained on checkpoints upstream.template.apply_anti_fee_sniping(tip_height, rng)/shuffle_outputs(rng)before emitting the PSBT.longterm_feeratelives onSelectParams(notSelectionStrategy::LowestFee): it feeds the waste-aware change policy for every strategy, and post-refactor!: ReplaceSelectorwith a singleinto_selectionpass #82 the bnb metric reads it fromSelectionParams.CandidatesError(stage 1),SelectError(stage 2),MissingKeyOrigin(add_global_xpubs).bdk_walletmaster @58fe631(the commit #502 is based on), pinned as a git dependency. Itstest-utilsneedstempfile >= 3.26, bumped in the lockfile.Tested via
wallet_tx/tests/three_stage.rs(SingleRandomDraw, DrainAll, LowestFee, change peek/reserve, locked-outpoint exclusion,add_global_xpubs, and theNoRecipientserror) against a deterministically-fundedbdk_wallet— nobitcoind.cargo fmt/clippy/doc/testare green across the workspace;bdk_txstill buildsno_std.Changelog notice
Before submitting