Skip to content

feat(selection)!: declare sighash_type on Plan-derived inputs#74

Open
evanlinjin wants to merge 2 commits into
bitcoindevkit:masterfrom
evanlinjin:feature/sighash_at_psbt_layer
Open

feat(selection)!: declare sighash_type on Plan-derived inputs#74
evanlinjin wants to merge 2 commits into
bitcoindevkit:masterfrom
evanlinjin:feature/sighash_at_psbt_layer

Conversation

@evanlinjin
Copy link
Copy Markdown
Member

@evanlinjin evanlinjin commented May 28, 2026

Closes #60.
Supersedes #69.

Description

The problem

Two issues with how PsbtParams::sighash_type worked.

1. The field itself wasn't very useful. It was a tx-wide uniform override. However, the realistic use cases for non-ALL sighashes are inherently per-input.

2. It didn't guard against the catastrophic Taproot case. Taproot signature size depends on sighash (SIGHASH_DEFAULT is 64 bytes and other Taproot sighashes are 65 bytes). A miniscript Plan has the sighash type pre-baked and Plan::satisfaction_weight() is only accurate if the signer uses the assumed sighash.

The solution

This PR adds an always-on safety auto-lock: any Plan whose Schnorr witness template includes a 64B signature gets TapSighashType::Default written, independent of declare_sighash.

We also replace PsbtParams::sighash_type with PsbtParams::declare_sighash: bool. When declare_sighash is set (default), SIGHASH_ALL is written to all PSBT inputs that are "weight-safe".

The new behavior is summarized below:

Plan's Schnorr placeholders declare_sighash: false declare_sighash: true (default)
Any 64B TapSighashType::Default (forced) TapSighashType::Default (forced)
All 65B unset TapSighashType::All
None (ECDSA) unset EcdsaSighashType::All

PSBT-derived inputs (Input::from_psbt_input) are never touched. They come in with their own sighash_type already set.

Callers needing non-ALL sighashes (SINGLE, NONE, *_ANYONECANPAY) set psbt.inputs[i].sighash_type directly on the returned PSBT. Plans needing non-ALL semantics on every key should be built with uniform TaprootCanSign::sighash_default = false so the safety lock doesn't fire.

Notes to the reviewers

This PR differs from #60's proposal.

The original issue suggested auto-writing DEFAULT only on uniformly-64B Plans (a plan can require multiple signatures with different sighash types). In this PR, a Plan with any 64B placeholder triggers sighash_type = DEFAULT.

  • Rationale: Mixed sighash types in one Plan are uncommon in practice. We want to avoid under-funding at all costs whereas over-funding is fine.

The original issue proposed writing None as default for "weight-safe" inputs. This PR sets declare_sighash = true as default, so that ALL will be written.

  • Rationale: BIP-174 requires finalizers to reject sigs that don't match a declared sighash_type - we use this as a defense. ALL/DEFAULT is what the super-majority of callers would want.

Alternative (rejected) solutions

  • Placing a sighash_type field in Plan-based bdk_tx::Input - This is a PSBT-concern. In fact, input witness can have multiple signatures.

Scope creep

I leaned up the tests a little beforehand: dca56db

Changelog notice

Added:
- `PsbtParams::declare_sighash` which writes `sighash_type = ALL` to all "weight-safe" inputs. This is the default that the super majority of callers would want.

Changed:
- Plan-based `Input`s whose witness template contains at least one 64B Schnorr signature will have `sighash_type = DEFAULT` written for that input, regardless of whether `PsbtParams::desclare_sighash` is set.

Removed:
- `PsbtParams::sighash_type` as transaction-wide sighash type setting is not useful.

Before submitting

@evanlinjin evanlinjin marked this pull request as ready for review May 28, 2026 06:21
@evanlinjin evanlinjin self-assigned this May 28, 2026
@evanlinjin evanlinjin force-pushed the feature/sighash_at_psbt_layer branch 3 times, most recently from 35f1b48 to 3ff0b79 Compare May 28, 2026 06:53
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.
@evanlinjin evanlinjin force-pushed the feature/sighash_at_psbt_layer branch from 3ff0b79 to f48fad7 Compare May 28, 2026 07:57
@ValuedMammal
Copy link
Copy Markdown
Collaborator

Thanks for the detailed description. I was in favor of the original proposal in #60 - remove the sighash_type field and auto-write DEFAULT if the plan's weight assumption depends on it. The behavior should be clearly documented. I think sighash type shouldn't be part of PsbtParams at all but handled by the signer/coordinator.

@evanlinjin
Copy link
Copy Markdown
Member Author

Thanks for the detailed description. I was in favor of the original proposal in #60 - remove the sighash_type field and auto-write DEFAULT if the plan's weight assumption depends on it. The behavior should be clearly documented. I think sighash type shouldn't be part of PsbtParams at all but handled by the signer/coordinator.

create_psbt plays the Creator + Updater roles, so PSBT_IN_SIGHASH_TYPE is its decision to make. The Signer role must honor what's declared, and Finalizers must reject signatures that don't match (they don't set such fields).

What's the motivation for PsbtParams::declare_sighash?

  • declare_sighash == true is what most callers want. They want their signatures to cover the whole transaction and we use PSBT_IN_SIGHASH_TYPE to enforce it.
  • declare_sighash == false - the caller saying "I want something custom, leave it to me". We still write DEFAULT for 64B Schnorr Plans because any other sighash will underfund the tx.

Copy link
Copy Markdown
Member

@notmandatory notmandatory left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

I like the approach to defaulting to "safe" sighash ALL and leaving it up to the user to manually op-out of the default and set their non-typical sighash types.

The coding is well documented and easy enough to follow. But can you add a test for the mixed sighash type concern described in problem 2 ? I don't doubt it will pass but would be good to have a unit test to make sure it doesn't break in the future.

@evanlinjin evanlinjin force-pushed the feature/sighash_at_psbt_layer branch from f48fad7 to 8a758f1 Compare May 28, 2026 23:59
Replaces `PsbtParams::sighash_type: Option<PsbtSighashType>` (a uniform
tx-wide override) with `PsbtParams::declare_sighash: bool` (default `true`).

A safety auto-lock always fires independent of `declare_sighash`: any Plan
whose Schnorr witness template includes a 64B signature gets
`TapSighashType::Default` written, preventing silent 64B-budget /
65B-sig under-funding. Mixed-size Plans land here too — the 65B-declared
keys are over-funded by 1B each rather than risk under-funding the
64B-declared ones.

For the remaining weight-safe inputs, `declare_sighash` controls whether to
declare the implicit BIP-174 default explicitly:

- `true`: write `TapSighashType::All` / `EcdsaSighashType::All` so
  finalizers enforce.
- `false`: leave unset, caller manages.

Breaking: callers using `PsbtParams::sighash_type` must move to
post-construction `psbt.inputs[i].sighash_type` mutation. Exhaustive
struct-literal construction of `PsbtParams` needs the new `declare_sighash`
field (or `..Default::default()`).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@evanlinjin evanlinjin force-pushed the feature/sighash_at_psbt_layer branch from 8a758f1 to afff4c6 Compare May 29, 2026 00:01
@evanlinjin
Copy link
Copy Markdown
Member Author

@notmandatory I added the mixed sighash test!

Copy link
Copy Markdown
Collaborator

@ValuedMammal ValuedMammal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NACK

@evanlinjin
Copy link
Copy Markdown
Member Author

evanlinjin commented May 29, 2026

@ValuedMammal welcome to NACK, might need to formulate proper arguments 😅

@evanlinjin
Copy link
Copy Markdown
Member Author

evanlinjin commented May 29, 2026

@ValuedMammal Let me present my reasoning clearly so it's easier for you to address. Here are my claims:

  1. PSBT_IN_SIGHASH_TYPE is meant to be set by the Updater (as per BIP-174). Signers consume it, they don't decide it. create_psbt plays both the Creator and Updater roles.

  2. Leaving PSBT_IN_SIGHASH_TYPE unset means the signer is free to use anything. BIP-174: "should sign using SIGHASH_ALL, but may use any sighash type they wish". A signer picking anything other than ALL/DEFAULT means inputs/outputs may be added/replaced.

  3. Declaring sighash binds compliant signers. BIP-174: "Signatures for this input must use the sighash type".

  4. The super-majority of callers want ALL/DEFAULT sighash types only. Therefore, we should have this as our default.

  5. 64B Schnorr Plans must be locked to DEFAULT otherwise we risk underfunding the transaction which is dangerous.

  6. declare_sighash: false preserves the "leave unset" behavior. Callers wishing for this behavior still have it.

Could you indicate which one you reject? Need to know what we are arguing about.

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.

PsbtParams::sighash_type does not make sense

3 participants