Skip to content

Take the committed script type from the script, not from which PSBT fields are set - #2

Merged
kwsantiago merged 1 commit into
unified-sighash-v0.8.0from
fix-unified-script-type
Sep 4, 2026
Merged

Take the committed script type from the script, not from which PSBT fields are set#2
kwsantiago merged 1 commit into
unified-sighash-v0.8.0from
fix-unified-script-type

Conversation

@kwsantiago

@kwsantiago kwsantiago commented Sep 4, 2026

Copy link
Copy Markdown

The unified message commits a byte naming the script type, which domain separates the four so a signature made for one can never verify as another. It was derived from which PSBT fields were present rather than from the spent output's script:

is_segwit = (inp.witness_script or inp.witness_utxo or ...)

BIP174 allows a legacy input to be carried as witness_utxo, and the or chain short-circuits on that before the actual script is ever consulted.

What went wrong

The same P2PKH output committed a different byte depending on how the PSBT carried it, demonstrated by execution:

non_witness_utxo digest: ac655fcb2690af9d42f918cff9b92fd1a508965ec694373ea95a1efcd65c67ff
witness_utxo     digest: f5dd823792cc509c2370aac2a91af24636aec04b0698f782fdb2a43b85f7fe8a

Consensus can only read that output as BARE, so the second signature cannot verify on chain. Two signers handed different carriers of one PSBT also disagreed with each other, and both differed from the other implementations of this message, which classify from the spent scriptPubKey.

The fix

Classify from the script, in both PSBT and PSBTView. The two clauses that already tested the real script are what remains, which matches the rule the reference implementation uses: witness types are p2wpkh and p2wsh natively, or wrapped where the redeem script is one of them; everything else is bare.

The legacy digest selection is deliberately untouched. It is upstream's, and only the unified message commits this byte to consensus.

Verified

The regression test fails against the previous code and passes against this one. Both carriers now produce the identical digest, and it is the BARE one that was already correct, so the case that moved is the one that was wrong.

Wrapped segwit is covered separately, to pin that dropping the presence clauses does not send P2SH-P2WPKH to BARE.

Worth noting for future review: the PSBT versus PSBTView agreement test does not catch this. Both implementations carried the same defect and agreed with each other throughout. Only comparing the two carriers of one UTXO exposes it.

155 tests pass.

On chain

A Knots regtest node past activation, spending the same P2PKH output through each carrier, signed by this library and finalized by it:

carrier before after
non_witness_utxo mined, 0x21 mined, 0x21
witness_utxo node refused the signature mined, 0x21

The digest also matches Knots' own test_framework.script.UnifiedSignatureHash, which is written from the specification rather than by mirroring an implementation, for the legacy input through both carriers and for a P2SH-P2WPKH input that must stay WITNESS_V0.

Harness: embit_legacy_carrier_e2e.py.

…ields are set

The unified message commits a byte naming the script type, which domain
separates the four so a signature made for one can never verify as another.
It was derived from field presence: witness_script or witness_utxo being set
meant WITNESS_V0.

BIP174 allows a legacy input to be carried as witness_utxo, and consensus can
only read that output as BARE. So the same P2PKH UTXO committed WITNESS_V0
through one carrier and BARE through the other, one of the two signatures could
not verify on chain, and two signers given different carriers of one PSBT
disagreed. It also broke multisig interop with the other implementations, which
all classify from the spent scriptPubKey.

Classified from the script now, in both PSBT and PSBTView. The legacy digest
selection is untouched: it is upstream's, and only the unified message commits
the byte to consensus.
@kwsantiago
kwsantiago merged commit 5f700aa into unified-sighash-v0.8.0 Sep 4, 2026
4 checks passed
@kwsantiago
kwsantiago deleted the fix-unified-script-type branch September 4, 2026 18:51
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.

1 participant