feat: introduce TxTemplate as an intermediate stage - #73
Conversation
This comment was marked as outdated.
This comment was marked as outdated.
27084ae to
e9c2962
Compare
e9c2962 to
de8bfc5
Compare
78bb570 to
0f1c8b2
Compare
noahjoeris
left a comment
There was a problem hiding this comment.
cACK 0f1c8b2
Nice improvements, thanks.
0e7d063 to
d5ef9f6
Compare
|
I'm not sold on the idea of TxTemplate I think it adds a layer of indirection with little benefit. |
|
@ValuedMammal Could you be a bit more specific on where you think the proposed API is problematic? Which costs are you weighing? |
|
I agree with the merits of this change. It detaches AFS, version, locktime and sequence from the PSBT creation, and TxTemplate ensures the invariants for a single party. |
Oh, the impression that TxTemplate is an indirection may have come from it being proposed as an intermediate stage; I can see that TxTemplate replaced the Selection type. The issue cites the anti-fee-sniping logic as an awkward API, but so far the shape of the API hasn't prevented me from creating PSBTs, so I don't know if I'm missing something. I'm also not satisfied with the AFS logic, but for different reasons
|
The nSequence path exists so the tx resembles an off-chain settlement spending a timelock path, and those carry nLockTime = 0. bitcoindevkit#65 dropped the `tx.lock_time = ZERO` that the path used to perform (correctly, since it regressed input CLTVs) but added no precondition in its place, so a tx whose locktime was already pinned — by an input's CLTV or by `min_locktime` — could come out with both a near-tip locktime and a confirmation-depth sequence. That matches neither an ordinary wallet spend nor a contract close: a third fingerprint, worse than either branch alone. Co-Authored-By: ValuedMammal <95981133+ValuedMammal@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d5ef9f6 to
04c5fc9
Compare
@ValuedMammal You're right that the API in The new Also mentioned already in the PR description (kinda):
In terms of the three points you raised:
|
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>
…iping` `apply_*` reads as an independent transformation that composes with the other shaping methods, but AFS shares the `lock_time` slot with `set_locktime`. `discourage_fee_sniping` reads as a decision rather than a pass, and matches Bitcoin Core's `DiscourageFeeSniping`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
04c5fc9 to
b3bdf7c
Compare
Both write `tx.lock_time`, so the rule needs saying: the value set is a floor, anti-fee-sniping may raise it toward the tip but never lowers it. Also notes the two consequences — a value within ~100 blocks of the tip leaves AFS's random backoff no room, and setting a locktime *after* AFS can undo the nSequence path's premise that `tx.lock_time` is zero. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
b3bdf7c to
a22218b
Compare
…fee-sniping discourage_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 concrete case this rules out: AFS may protect the transaction by setting an input's nSequence instead of the locktime, which it only does while lock_time is zero. A set_locktime afterwards would rebuild a tx carrying both a near-tip locktime and a confirmation-depth sequence — the fingerprint bitcoindevkit#87 exists to prevent. A compile_fail doctest pins that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Description
Closes #57. Based on #87.
PsbtParamswas doing two jobs: it carried bitcoin-transaction-shape fields (version,min_locktime, sequence) and PSBT emission options. Anti-fee-sniping depended on the tx-shape half, so it was structurally pinned insidecreate_psbt— which meant every caller reckoned with two AFS error variants and an extra input whether they wanted AFS or not.AFS isn't a PSBT concern. It decides a transaction's locktime and input sequences, so it belongs before emission — as in Bitcoin Core, where
DiscourageFeeSnipingruns as a step before the transaction is built. It couldn't move without first splitting the tx-shape fields out ofPsbtParams.This PR renames
SelectiontoTxTemplateand gives it those fields, so the tx shape is owned and observable before anything is emitted:Mostly ergonomics, with one exception. On
master,PsbtParams { version: ONE, .. }with a relative-timelock input builds a v1 transaction whoseOP_CSVcan never be satisfied — BIP-68 sequence locks only apply from v2 — and you find out when it's rejected. That's nowSetVersionError::RelativeTimelockRequiresV2.Notes to the reviewers
Commit 1 is a pure rename —
TxTemplateisSelection, not a new layer between it and the PSBT.Three commits at the end respond to review:
discourage_fee_sniping(wasapply_anti_fee_sniping).apply_*reads as an independent transformation that composes with the other shaping methods, but AFS shares thelock_timeslot withset_locktime. Also matches Core's naming.set_locktimeis a floor:tx.lock_timeonly moves up from there, AFS may raise it toward the tip but never lowers it — the same way input CLTVs already compose. I considered making AFS authoritative instead (last-write-wins) and decided against it: having AFS silently discard a value the caller explicitly set is the footgun this PR exists to remove, and it would make AFS the one operation inTxTemplatethat can lowerlock_time.discourage_fee_snipingconsumes the template and returns aSealedTxTemplateexposing only reads and emission. This is narrower than it sounds — only AFS seals, soset_locktimebefore it still works. It rules out one concrete case: AFS may protect the transaction by setting an input'snSequencerather than the locktime, which it only does whilelock_timeis zero, and aset_locktimeafterwards would rebuild a transaction carrying both a near-tip locktime and a confirmation-depth sequence — exactly the fingerprint fix(afs): require zero locktime for the nSequence path #87 prevents. Acompile_faildoctest pins it.Points 1 and 3 of #73 (comment) are not addressed here; point 2 is #87, which this builds on.
Changelog notice
SelectiontoTxTemplate, now the single workspace for transaction shaping (version, locktime, fallback sequence, per-input sequence, ordering, anti-fee-sniping, emission).Selector::try_finalizenow returnsOption<TxTemplate>;InputCandidates::into_selectionis nowinto_tx_template, returningResult<TxTemplate, IntoTxTemplateError>(wasIntoSelectionError).PsbtParamsintoBuildPsbtParams(emission-only). Tx-shape options moved toTxTemplatesetters:set_version,set_locktime,set_fallback_sequence,discourage_fee_sniping.build_psbt(renamed fromcreate_psbt) and returns(Psbt, Finalizer); its params/error areBuildPsbtParams/BuildPsbtError.shuffle_inputsis now consuming (returnsSelf).TxTemplate::discourage_fee_sniping(was thePsbtParams::anti_fee_snipingfield). It andset_locktimewrite the sametx.lock_timeslot and compose monotonically: a value passed toset_locktimeis a floor that AFS may raise toward the chain tip but never lowers.discourage_fee_snipingconsumes theTxTemplateand returns a newSealedTxTemplatethat exposes only reads and emission, so the tx shape cannot be mutated after AFS.TxTemplatederefs toSealedTxTemplate.min_locktimewas silently ignored and a below-CLTV value silently clamped;set_locktimenow returnsSetLockTimeError::UnitMismatch/BelowInputCltv, andset_versionreturnsSetVersionError::RelativeTimelockRequiresV2. The hardcodedENABLE_RBF_NO_LOCKTIMEfallback sequence is now configurable viaset_fallback_sequence(default unchanged).Before submitting