Skip to content

feat: introduce TxTemplate as an intermediate stage#73

Draft
evanlinjin wants to merge 8 commits into
bitcoindevkit:masterfrom
evanlinjin:feature/tx-template
Draft

feat: introduce TxTemplate as an intermediate stage#73
evanlinjin wants to merge 8 commits into
bitcoindevkit:masterfrom
evanlinjin:feature/tx-template

Conversation

@evanlinjin
Copy link
Copy Markdown
Member

@evanlinjin evanlinjin commented May 20, 2026

Summary

Closes #57. Refactors Selection into a richer tx-shape type called TxTemplate. The pipeline becomes:

Selector::try_finalize() → TxTemplate → (Psbt, Finalizer) | Transaction

TxTemplate is the single workspace for how the transaction is shaped — version, locktime, fallback sequence, per-input sequence overrides, ordering, anti-fee-sniping, and final emission. AFS is no longer privileged: it's a pure caller of public methods (set_locktime, Input::set_sequence).

New templates start with sensible defaults (version = v2, lock_time = max input CLTV (or ZERO), fallback_sequence = ENABLE_RBF_NO_LOCKTIME). All overrides are methods: set_version, set_locktime, set_fallback_sequence, apply_anti_fee_sniping, shuffle_inputs, sort_outputs_by, input_mut, etc.

Migration

Before After
Selection TxTemplate
Selector::try_finalize() -> Option<Selection> Selector::try_finalize() -> Option<TxTemplate>
InputCandidates::into_selection(algo, params) InputCandidates::into_tx_template(algo, params) -> Result<TxTemplate, _>
IntoSelectionError IntoTxTemplateError
PsbtParams PsbtBuildParams (emission-only)
PsbtParams::version TxTemplate::set_version(v) -> Result<_> (rejects v < 2 if any input has a relative timelock)
PsbtParams::min_locktime TxTemplate::set_locktime(lt) -> Result<_> (validates same-unit + ≥-CLTV)
PsbtParams::anti_fee_sniping: Option<Height> TxTemplate::apply_anti_fee_sniping(tip, &mut rng) -> Result<_>
selection.create_psbt(p)? tx_template.create_psbt(PsbtBuildParams::default())? returning (Psbt, Finalizer)
selection.shuffle_inputs(&mut rng) (&mut self) tx_template.shuffle_inputs(&mut rng) (consuming, returns Self)
selection.sort_inputs_by(...) tx_template.sort_inputs_by(...)
selection.input_mut(op) / inputs_mut() tx_template.input_mut(op) / inputs_mut()
(hardcoded ENABLE_RBF_NO_LOCKTIME) TxTemplate::set_fallback_sequence(s); default unchanged

Behaviour changes worth flagging:

  • What used to be silent is now an explicit error. min_locktime of the wrong unit was silently ignored; below-CLTV was silently clamped up. Now set_locktime returns SetLockTimeError::UnitMismatch / SetLockTimeError::BelowInputCltv. Trying to set v1 with a relative-timelock input returns SetVersionError::RelativeTimelockRequiresV2.
  • The hardcoded Sequence::ENABLE_RBF_NO_LOCKTIME fallback is now configurable via set_fallback_sequence.
  • Library no longer depends on rand. Only rand_core (for the RngCore trait). Production dep tree: miniscript, bdk_coin_select, rand_core.

Out of scope

  • no_std feature. Blocked on bdk_coin_select upstream: fix: add conditional FloatExt import for no_std builds coin-select#37 (FloatExt import fix) is merged but no 0.4.2 release exists yet. Follow-up once a release lands.
  • PSBT v2 support. Design accommodates it cleanly as a future TxTemplate::create_psbt_v2() method — same construction logic, different emit step.

Notes for reviewers

  • apply_anti_fee_sniping calls set_locktime / Input::set_sequence via the public path and .expect()s the results. The expects hold by construction:
    • locktime path: only fires when afs_locktime >= current (both height-based, since AFS early-rejects time-based), so set_locktime's unit + ≥-CLTV checks always pass.
    • sequence path: only picks taproot inputs without a relative timelock, so set_sequence cannot fail the CSV/CLTV-disable check.
  • TxTemplate::from_parts (the constructor used by Selector::try_finalize) is pub(crate). External users construct via Selector / InputCandidates. If hand-rolled TxTemplate becomes a common ask, exposing this is a one-line change.

Commits (3)

  1. `refactor!: rename Selection to TxTemplate` — pure rename, no behaviour change. Sets up commit 2.
  2. `feat(tx-template)!: route tx-shape decisions through TxTemplate` — the meat. Adds the resolved tx-shape fields (`version`, `lock_time`, `fallback_sequence`), the corresponding setters with validation (`set_version`, `set_locktime`, `set_fallback_sequence`), and the refactored PSBT/AFS pipeline (`PsbtParams` → `PsbtBuildParams`, `create_psbt` returns `(Psbt, Finalizer)`, AFS becomes a chainable `apply_anti_fee_sniping` step calling public methods). Closes Introduce TxTemplate as a state between Selection and Psbt #57.
  3. `refactor!: drop rand dependency from the library` — `create_psbt` no longer needs an RNG; `rand` moves to dev-deps. Production tree is now `miniscript`, `bdk_coin_select`, `rand_core`.

Test plan

  • CI: `cargo fmt --check`, `cargo clippy --all-features`, `cargo check --all-features --tests --examples`, `cargo test --all-features`
  • Doc tests pass (the synopsis example in `finalizer.rs` was updated to the new API)
  • Manual run of `examples/synopsis` and `examples/anti_fee_sniping` against bitcoind via `bdk_testenv`
  • Verify no transitive `rand` dep in `cargo tree` against the published library

evanlinjin and others added 5 commits May 19, 2026 15:10
* move no-std rand functions to `no_std_rand.rs`
* move AFS types to `afs.rs`
* Add `fisher_yates_shuffle` function that shuffles list elements
`Selection.inputs` / `Selection.outputs` are now private; access through
`inputs()` / `outputs()`. Mutation goes through `InputMut`, returned by
`input_mut(outpoint)` and `inputs_mut()`. `InputMut` derefs to `&Input`
and only exposes `set_sequence`, preventing whole-input replacement that
would silently break coin-selection invariants.
Adds `sort_inputs_by`, `shuffle_inputs`, `sort_outputs_by`, `shuffle_outputs`
on `Selection`. Covers BIP-69 ordering (via sort_by) and chain-analysis
shuffling without exposing raw mutable slots that would let callers replace
inputs/outputs and silently break coin-selection invariants.
`SelectorError::LockTypeMismatch` now rejects candidate inputs with mixed
absolute-timelock units up front, in `Selector::new`. `CreatePsbtError::LockTypeMismatch`
is removed and `accumulate_max_locktime` becomes infallible (with a
`debug_assert!` guarding the upstream invariant). Catches the failure at
the earliest point and lets PSBT creation assume the invariant.
Co-authored-by: Noah Joeris <noahjoeris@gmail.com>
@evanlinjin
Copy link
Copy Markdown
Member Author

This is fully AI generated and not ready for review.

@evanlinjin evanlinjin force-pushed the feature/tx-template branch 2 times, most recently from 8b53dee to 27084ae Compare May 20, 2026 05:09
evanlinjin and others added 3 commits May 20, 2026 05:19
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>
@evanlinjin evanlinjin force-pushed the feature/tx-template branch from 27084ae to e9c2962 Compare May 20, 2026 05:21
@evanlinjin evanlinjin mentioned this pull request May 27, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce TxTemplate as a state between Selection and Psbt

1 participant