Skip to content

Merge upstream: input amount/script verification, OP_RETURN pushes, nested change - #485

Open
3rdIteration wants to merge 56 commits into
devfrom
merge-upstream-psbt-input-verification
Open

3rdIteration wants to merge 56 commits into
devfrom
merge-upstream-psbt-input-verification

Conversation

@3rdIteration

@3rdIteration 3rdIteration commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Merges the 53 new commits from SeedSigner/seedsigner dev (merge-base cfaf443a):

The rule throughout: follow upstream unless that would weaken security. The real merge commit holds the conflict resolution. The commits after it add the test vectors.

What was kept, what was adopted

Upstream Fork before Result
SeedSigner#995 input amounts _validate_input already did all of it and more: MAX_MONEY ranges, SIGHASH_ALL only, rejecting a witness_utxo on a non-segwit prevout, the totals check, and the UNVERIFIED_INPUT_AMOUNTS advisory (upstream accepts CVE-2020-14199 silently) Fork kept. Upstream's verified_input_amount name is adopted, and input_amount stays as an alias. InputScope.is_verified is now set as well.
SeedSigner#1041 negative fee NEGATIVE_FEE plus the totals check Fork kept
SeedSigner#1047 input scripts Same hash checks Fork kept, plus upstream's extras: a stray redeem_script on a non-p2sh input is refused (new EXTRANEOUS_REDEEM_SCRIPT), and an absent script is now MISSING_INPUT_SCRIPT instead of being reported as a hash mismatch
SeedSigner#1042 OP_RETURN Refused OP_1NEGATE/OP_RESERVED/OP_1..OP_16, so every Runes runestone (OP_RETURN OP_13 …) was refused Upstream adopted (_parse_op_return_payload, matching Core's IsPushOnly). Malformed scripts are refused as the new MALFORMED_OP_RETURN. The fork's multi-payload list and burn handling are unchanged.
SeedSigner#1046 nested change Redeem-less p2sh-p2wpkh change showed up as a payment. A redeem-less output that claimed our key but paid a stranger also parsed silently as a payment Upstream adopted (_is_change_candidate). The rebuild now proves the first case is change and refuses the second as FORGED_OUTPUT_OWNERSHIP.
SeedSigner#1065 Swahili n/a Taken as is. seedsigner-translations has no l10n/sw yet, so it falls back to English until it does. Upstream is in the same position.

How the conflicts were resolved

  • Errors: upstream's PSBT*Error classes map onto InvalidPSBTError + RejectCode, as in Merge upstream: multisig output claim hardening + high fee warning #469. Upstream's View classes are PSBTRefusalView subclasses with the same names, so upstream's flow tests and screenshot configs resolve. A new also_codes attribute lets one view stand for several codes.
  • Refusal screens: before this, these refusals rendered as the generic "Invalid PSBT" screen with untranslated parser text. REJECT_PRESENTATION now has entries for them, using upstream's msgids:
    • Dire, "Likely an Attack!": UTXO_MISMATCH, INVALID_WITNESS_UTXO, SCRIPT_HASH_MISMATCH
    • Warning: MISSING_UTXO, MISSING_INPUT_SCRIPT, EXTRANEOUS_*_SCRIPT, NEGATIVE_FEE, MALFORMED_OP_RETURN
  • Tests: upstream's new parser and flow tests are ported to raises_reject. Where the fork returns a more specific code, the assertion follows the fork and a comment explains the difference: INVALID_WITNESS_UTXO, UTXO_MISMATCH for an out-of-range vout, MISLABELED_OUTPUT_OWNERSHIP, EXTRANEOUS_WITNESS_SCRIPT. PSBTParserOwnershipTestBase is split back out, as upstream has it. TestPSBTParserOutputOwnership no longer re-runs the seed-ownership tests by inheriting them.

New test vectors

28 corpus fixtures, with _V2 twins where the trap isn't version-specific. All are generated by psbt_faker's new builders and vendored with generate.py --keep-existing. Each builder seeds its RNG from its test id, so the bytes are reproducible. PROVENANCE.md records them. (An earlier commit on this branch derived 24 of them locally; those are now replaced by the psbt_faker output.)

Vector On dev After
XTRAS.OP_RETURN_RUNESTONE, _SMALLINT refused (UNDISPLAYABLE_OUTPUT) parses, payload shown exactly
XTRAS.OP_RETURN_MULTI_PUSH parses parses
XTRAS.OP_RETURN_TRUNCATED_PUSH, _NON_PUSH UNDISPLAYABLE_OUTPUT MALFORMED_OP_RETURN
XTRAS.STRAY_REDEEM_SCRIPT parses EXTRANEOUS_REDEEM_SCRIPT
XTRAS.MISSING_REDEEM_SCRIPT, XTRAS.MISSING_WITNESS_SCRIPT SCRIPT_HASH_MISMATCH MISSING_INPUT_SCRIPT
XTRAS.WRONG_REDEEM_SCRIPT, XTRAS.FOREIGN_INPUT_BAD_SCRIPT SCRIPT_HASH_MISMATCH SCRIPT_HASH_MISMATCH
XTRAS.NESTED_CHANGE_NO_REDEEM parses, change shown as a payment parses, counted as change
TX-21.nested_bare_p2sh_claims_us_pays_other parses as an ordinary payment FORGED_OUTPUT_OWNERSHIP
XTRAS.ZERO_FEE parses parses
XTRAS.MS_NESTED_HONEST (2-of-3 p2sh-p2wsh at m/48h/0h/0h/1h) parses, change found parses, change found
XTRAS.MS_NESTED_WRONG_WITNESS_SCRIPT (inner layer only) / _WRONG_REDEEM_SCRIPT (outer layer only) SCRIPT_HASH_MISMATCH SCRIPT_HASH_MISMATCH (regression coverage)
XTRAS.MS_NESTED_MISSING_WITNESS_SCRIPT SCRIPT_HASH_MISMATCH MISSING_INPUT_SCRIPT

Testing

  • test_psbt_parser.py, test_psbt_test_suite.py, test_flows_psbt.py, test_flows_psbt_malicious.py, test_psbt_refusal_screens.py, test_psbt_multisig_key_paths.py: 1257 passed. The 56 skips are all fixed-prose codes, covered by test_table_prose_fits. The new refusal text fits on screen (test_table_prose_fits).
  • Full suite on the merge commit: 2535 passed, 6 failed. All 6 fail the same way on dev in this environment: 5 l10n tests (no compiled catalogs locally) and test_real_connector_has_satodime_ndef_v2.
  • Not run: the screenshot generator, because libraqm isn't installed here. Instead I checked that all 25 psbt_views classes it references exist and render with the intended screen class. Nothing was tested on hardware.

Follow-ups

  • Fork translations for the new refusal strings. Most are upstream's msgids, so they come in with upstream's catalogs.
  • Upstream's payjoin and finalized-input TODOs are still open in both trees.

🤖 Generated with Claude Code

newtonick and others added 30 commits September 19, 2026 22:43
PSBTParser summed each input's declared amount without ever checking it,
so the fee shown on the overview and math screens was only a claim from
the coordinator. _verify_input_amounts() now runs in parse() and raises
PSBTInputAmountVerificationError when an input has no utxo data, when its
outpoint index falls outside the non_witness_utxo it claims to spend, when
that non_witness_utxo does not hash to the txid it claims to spend, when a
legacy or unrecognized input supplies no non_witness_utxo, or when a
witness_utxo disagrees with a verified non_witness_utxo. PSBTOverviewView
catches it, clears the parser, and routes to a discard-only
PSBTInputAmountVerificationFailedView (approach and placement from SeedSigner#977).

The check runs after the ownership checks so a psbt this seed cannot sign
is reported as a mismatch rather than as a suspicious transaction, and
before _parse_inputs, where amounts are first summed. Segwit inputs
carrying only a witness_utxo are still accepted, since BIP-143 commits the
amount they sign and many coordinators never sends both utxo fields

Co-Authored-By: 0kPN1 <55192256+0kPN1@users.noreply.github.com>
A p2sh-p2wpkh output's scriptPubKey is a bare p2sh hash. BIP-174 leaves the
redeem script optional on an output, and without it _get_policy types the output
as plain p2sh, which does not match a p2sh-p2wpkh wallet policy. The output then
never reaches the ownership check at all. Two things follow. The user's own
change is displayed as a payment out to a stranger. And an output that keeps a
genuine claim on this seed while repointing its scriptPubKey escapes the
ownership-contradiction refusal by dropping one optional field.

Nested single sig is the only script type whose identity depends on an optional
field its proof does not read: the rebuild is p2sh(p2wpkh(K)) from our own seed
at the claimed path and the inputs' policy, so the missing field is a
discriminator rather than evidence. For p2sh-p2wsh the discriminator and the
proof material are the same field, so its absence is genuine silence and
correctly out of reach.

_policy_shape_matches becomes _is_change_candidate, an instance method, since it
now reads the inputs' policy and the output's verified derivation paths. Beside
the unchanged shape comparison it admits a bare p2sh output under a p2sh-p2wpkh
wallet that carries no m-of-n, supplies exactly one derivation path entry, and
holds one verified claim on this seed. Everything below is unchanged: the
rebuild decides, honest change is counted as change, and a repointed output
raises PSBTOutputOwnershipContradictionError.

The single-entry conditions are what keep that refusal safe. A genuine multisig
output carries one derivation path entry per cosigner, so it never reaches the
contradiction check by this route. The shape that would is a bare p2sh output
withholding both scripts while annotating a single key of a multisig, and no
surveyed coordinator emits one. Every coordinator but Bitcoin Core annotates
only its own change output. Core annotates an output because a descriptor solved
its script, so it holds all n key origins and writes them.
The nested single sig outlier in change candidacy also required the
output's one derivation path entry to claim this seed's fingerprint.
That kept an output paying our key under another seed's key and
fingerprint out of the rebuild, so it was shown as a spend to the
user's own address, while the same output with its redeem script
present is refused as a contradiction. The rebuild derives our key at
the entry's path whatever fingerprint it lists, so the outlier drops
the condition and _is_change_candidate drops the verified derivation
paths it read for it.

The outlier also required exactly one derivation path entry. It only
kept out bare p2sh outputs annotated with several keys while omitting
their scripts, which no surveyed coordinator emits, and the single sig
surplus check now refuses those.

The m-of-n exclusion becomes a check that the output omits its redeem
script. For an output parsed as plain p2sh the two admit the same
outputs, since a supplied multisig redeem script is the only source of
an m-of-n there, and the new check states what the outlier is for.

New tests refuse an output that pays this seed but lists another key,
and pin each of the three remaining conditions: a payment to another
wallet of this seed, annotated with our key, stays a spend.
Renaming _policy_shape_matches to _is_change_candidate dropped the
reason the candidacy test compares shape alone. Without it, adding the
cosigners to the comparison reads as a harmless tightening.

The old wording named a misannotated fingerprint as what breaks an
output's cosigner resolution. Since SeedSigner#1032, _get_cosigners matches each
key to a global xpub by derivation path alone and never reads the
fingerprint, so the restored note names a misannotated derivation path
instead.
…inglesig_change

[security] Verify nested single sig change that omits its redeem script
Co-authored-by: kdmukai <934746+kdmukai@users.noreply.github.com>
Co-authored-by: kdmukai <934746+kdmukai@users.noreply.github.com>
Co-authored-by: kdmukai <934746+kdmukai@users.noreply.github.com>
Co-authored-by: kdmukai <934746+kdmukai@users.noreply.github.com>
Co-authored-by: kdmukai <934746+kdmukai@users.noreply.github.com>
[security] Verify PSBT input amounts before displaying the fee
Chaitanya-Keyal and others added 26 commits October 22, 2026 01:59
embit computes the fee as inputs minus outputs and never looks at the sign of
the result, so a psbt that spends more than it funds parses cleanly and reaches
the review flow quoting a negative fee.

Raise PSBTNegativeFeeError as soon as the fee is known and route it to its own
warning. Malformed rather than hostile: no honest coordinator builds one and
the network would refuse to relay it, so it gets a plain warning, not the
attack framing. A zero fee is still accepted.
The error's docstring argued about a lying coordinator's motives and why
the error exists for the user. Like the other PSBTVerificationError
subclasses, it now just states the problem and that it is not treated as
an attack. The one part of the reasoning that matters, that both totals
are the coordinator's own claims, moves to the check that raises it.
_verify_input_amounts() now checks every input's amount before the outputs
are parsed, so the input total is no longer just the coordinator's claim.
…-fee

Reject psbts whose outputs exceed their inputs
The payload was read at a fixed 3-byte offset, which only holds when the script
uses OP_PUSHDATA1. Opcodes 0x01-0x4b are themselves the byte count, and that is
the minimal encoding Bitcoin Core emits, so every payload of 75 bytes or fewer
lost its first byte:

    scriptPubKey  6a 28 "Chancellor on the brink of third bailout"
    parsed        b'hancellor on the brink of third bailout'

PSBTOpReturnView is a pre-signing review screen, so the user approved a
transaction whose OP_RETURN content was displayed wrong. When the payload is not
human-readable it renders as hex, where a one byte shift is invisible.

Read the push opcode and slice to the declared length, in a static helper so the
loop stays readable. OP_PUSHDATA2 and OP_PUSHDATA4 are handled too: Bitcoin Core
v30 raised the default -datacarriersize to 100,000 bytes, and consensus never
limited OP_RETURN data at all, so a payload past what OP_PUSHDATA1 can describe
is no longer exotic.

Several pushes after OP_RETURN are legal and are concatenated; the user is being
shown the data the transaction commits to, and the push boundaries carry no
consensus meaning. Anything that is not a data push, or a push that overruns the
script, ends the walk and the remaining bytes are surfaced verbatim rather than
dropped.

Also reset op_return_data in _parse_outputs, which only ever set it in
__init__, and compare the first byte by slice so an empty scriptPubKey does not
raise IndexError.

Fixes SeedSigner#963

Co-authored-by: ruipereira1 <159703278+ruipereira1@users.noreply.github.com>
Co-authored-by: kwsantiago <44934418+kwsantiago@users.noreply.github.com>
Every existing OP_RETURN vector hand-built OP_PUSHDATA1, which is exactly the one
encoding the parser assumed, so the mis-slice never showed up. The regtest psbt
behind test_parse_op_return_content carries 2b 6a 4c 28 43 68 61 6e ... and the
screenshot generator concatenated OP_PUSHDATA1 by hand.

Add fixture helpers that build an OP_RETURN scriptPubKey with the minimal push by
default, and that can force a specific OP_PUSHDATA* so the non-minimal encodings
a coordinator is free to produce are testable too. Note in the helper that the
scriptPubKey and value have to be set on the OutputScope: PSBT.tx rebuilds the
transaction from the scopes on every access, so assigning through psbt.tx.vout[i]
is silently discarded and leaves a test asserting against the fixture it meant to
replace.

The new tests cover the direct-push/OP_PUSHDATA1 boundary at 75/76, OP_PUSHDATA2
and OP_PUSHDATA4, a non-minimal encoding, several pushes in one script, a bare
OP_RETURN, an empty scriptPubKey, a truncated length field, an over-long declared
length, and a leading opcode that is not a data push.

Co-authored-by: ruipereira1 <159703278+ruipereira1@users.noreply.github.com>
Co-authored-by: kwsantiago <44934418+kwsantiago@users.noreply.github.com>
The generator concatenated OP_PUSHDATA1 unconditionally, so its 49-byte payload
was encoded a way Bitcoin Core would never produce. Push directly at 75 bytes or
fewer and via OP_PUSHDATA1 above that, matching the parser. The rendered
screenshots are unchanged; the vector behind them is no longer wrong.

Co-authored-by: kwsantiago <44934418+kwsantiago@users.noreply.github.com>
`op_return_script`'s `push_opcode` and `create_op_return_psbt`'s `change_amount` and
`fee_amount` were never used by any test. The non-minimal encoding cases build their
bytes by hand and call `_parse_op_return_payload` directly.
Bitcoin Core treats an OP_RETURN output as nonstandard unless everything after the
opcode is a push. The parser used to display whatever was left when a push ran past the
end of the script, or when it met an opcode that is not a push. It now raises
PSBTMalformedOpReturnError instead, and the user gets the same plain "Transaction
Problem" warning used for other malformed psbts before discarding it.

OP_0, OP_1NEGATE, OP_RESERVED, and OP_1 through OP_16 still count as pushes to Core, so
they are kept and the walk carries on past them rather than stopping there.
…h-opcode

Parse the OP_RETURN push opcode instead of assuming OP_PUSHDATA1
BIP-174 asks a signer to hash an input's redeem or witness script
against the scriptPubKey being spent. We read the wallet policy (m-of-n,
cosigner keys) out of those scripts and never checked them, so a
coordinator's own script decided which outputs we displayed as the
user's change.

_verify_input_scripts runs on every input before the policy is read, and
each refusal has its own screen:

- A missing script raises PSBTMissingInputScriptError, graded as a
  correctness problem.
- A script that hashes to the wrong value raises
  PSBTInputScriptMismatchError, graded as an attack. It is the same
  contradiction between a supplied script and its scriptPubKey that
  PSBTOutputOwnershipContradictionError grades as an attack on outputs.
- An extraneous script (a witness script on a legacy multisig input, a
  redeem script on a native segwit input) raises
  PSBTExtraneousInputScriptError, left ungraded. It is refused on every
  input type: there is no hash to check it against, and _get_policy
  decides a p2sh input is nested segwit from the mere presence of a
  witness script.

Measured across the four p2sh and p2wsh fixtures: no signature over a
wrong script was valid against the real coin, because embit builds the
digest from the first supplied script (witness, then redeem, then
scriptPubKey). Funds were never movable this way; what the check closes
is the display.
The _verify_input_scripts docstring said BIP-174 requires the psbt to
supply an input's redeem or witness script. BIP-174 allows a psbt to be
passed between roles before every script is known, with each Updater
adding the scripts it knows. A signer still needs an input's script to
produce a valid signature for that input. BIP-174's signer rules check a
script once it is provided. The mismatch and extraneous checks enforce
those rules.

The docstring now also groups the checks by the inputs they cover. The
missing and mismatch checks run only on p2sh and p2wsh inputs. The
extraneous script check runs on every input.

Docstring-only change.
A payjoin receiver's input arrives finalized (BIP-78), and finalizing
clears its redeem and witness script fields (BIP-174). So a nested
segwit payjoin is refused as missing its input script; before input
script verification it was refused as "Mixed inputs". Native segwit
payjoins parse either way.

Two TODOs mark what payjoins and other collaborative spends would need:
- recover a finalized input's script from its final scriptSig or witness
- read the wallet policy only from the user's own inputs

The every-input test's docstrings now describe its fixture by the rule
it tests rather than as a payjoin input. It is an unfinalized input that
carries its redeem script, built to check that every input's scripts
are verified whoever the psbt says the input belongs to.

Comment-only change.
The comment above the _get_policy call in _parse_inputs said the
input's scripts could now be used "safely". The check before it binds
each script to the scriptPubKey the psbt supplies, and nothing in the
parse authenticates that scriptPubKey yet, so the word claims more
than the check establishes.

The shared test base class described itself as the base for the two
ownership test classes. The input script tests are a third subclass,
and not an ownership test class.

Comment-only change.
TestPSBTParserInputScripts carried a copy of _foreign_multisig_script
identical to the one in TestPSBTParserOutputOwnership. Both classes now
inherit it from PSBTParserOwnershipTestBase.

The base class docstring listed the fixtures the class body defines.
Adding this helper would have meant extending that list. It now names
the tests the fixtures support: how PSBTParser verifies a psbt's claims
about its inputs and outputs. That covers the input script tests too,
which check an input's scripts whoever owns it.

Test-only change.
The docstring now lists the method's two steps in the order they run:
verifying each input's scripts and determining the wallet policy.

Docstring-only change.
To decide whether an input's sighash commits its amount, the amount
check reads the input's script type, and telling nested segwit from
legacy p2sh reads the input's redeem and witness scripts. Those scripts
were verified only later, in _parse_inputs. So a witness-only nested
segwit input missing one of its scripts read as legacy p2sh and was
refused with PSBTInputAmountVerificationError, graded as an attack,
instead of PSBTMissingInputScriptError.

The script check now runs inside the amount check's loop: after the
checks on an input's previous transaction, so a forged one is still
refused as an attack, and before the check that reads the scripts. The
method is renamed _verify_input_amounts_and_scripts to match, and the
per-type utxo requirements move to the comment on
UNCOMMITTED_AMOUNT_SCRIPT_TYPES.

The foreign nested segwit input in the every-input test now carries a
witness_utxo alone, so its missing-redeem-script case takes the
witness-only path. The test fails on the previous order.
The docstring and inline comments in _verify_input_amounts_and_scripts
ran to 104 columns. They are now wrapped at 90, with no wording changes
apart from a closing period on the two comments that now span two
lines.

Comment-only change.
parse()'s docstring is a high-level overview of the steps.
_verify_input_amounts_and_scripts and _verify_input_scripts each end
their docstring with a list of every error they raise, and name the
errors only there.
…ripts

[security] Verify input scripts against the scriptPubKey they commit to
…s, nested change

Brings in SeedSigner/seedsigner SeedSigner#995 (verify input amounts), SeedSigner#1046 (nested
single sig change without its redeem script), SeedSigner#1041 (reject a negative fee),
SeedSigner#1042 (parse the OP_RETURN push opcode), SeedSigner#1047 (verify input scripts) and
SeedSigner#1065 (enable Swahili).

Conflict resolution, keeping the fork's behaviour wherever it is equal or
stronger:
- Input amounts (SeedSigner#995) and negative fee (SeedSigner#1041): the fork's _validate_input and
  NEGATIVE_FEE already cover upstream's checks and more (MAX_MONEY ranges,
  SIGHASH_ALL only, witness_utxo on a non-segwit prevout, the totals check, the
  UNVERIFIED_INPUT_AMOUNTS advisory). Upstream's _verify_input_amounts_and_scripts
  is dropped. Upstream's verified_input_amount name is adopted, assigned only once
  every input passes; input_amount stays as a read-only alias. _validate_input
  now also calls embit's inp.verify(), so InputScope.is_verified is set.
- Input scripts (SeedSigner#1047): the fork's hash checks are kept. Adopted from upstream:
  a stray redeem_script on a non-p2sh input is refused (new
  EXTRANEOUS_REDEEM_SCRIPT), and an absent script is reported as the new
  MISSING_INPUT_SCRIPT rather than as a hash mismatch. Upstream's finalized-input
  and payjoin TODOs are carried over.
- OP_RETURN (SeedSigner#1042): adopted upstream's _parse_op_return_payload. OP_0,
  OP_1NEGATE, OP_RESERVED and OP_1..OP_16 count as pushes, as in Core's
  IsPushOnly. The fork refused these, which meant every Runes runestone
  (OP_RETURN OP_13 ...). A malformed one raises the new MALFORMED_OP_RETURN.
  The fork's per-output payload list and burn handling are kept.
- Nested change (SeedSigner#1046): adopted upstream's _is_change_candidate, so redeem-less
  p2sh-p2wpkh change is proven by rebuild instead of being shown as a payment.
  _get_script_type is adopted as well.
- Errors: upstream's PSBT*Error classes map onto InvalidPSBTError + RejectCode,
  as in the previous merge. Upstream's View classes become PSBTRefusalView
  subclasses with the same names. A new also_codes attribute lets one view
  stand for more than one code. REJECT_PRESENTATION gains entries for the input
  amount, script, negative fee and OP_RETURN refusals, using upstream's msgids.
  The deceptive ones (UTXO_MISMATCH, INVALID_WITNESS_UTXO, SCRIPT_HASH_MISMATCH)
  are Dire / "Likely an Attack!"; the malformed ones are plain warnings. Before,
  they fell through to the generic "Invalid PSBT" screen with untranslated
  parser text.
- Tests: upstream's new parser and flow tests are ported to raises_reject. Where
  the fork returns a more specific code, the assertion is changed and the
  difference is commented: INVALID_WITNESS_UTXO for legacy-without-prevtx,
  UTXO_MISMATCH for an out-of-range vout, MISLABELED_OUTPUT_OWNERSHIP, and
  EXTRANEOUS_WITNESS_SCRIPT. PSBTParserOwnershipTestBase is split back out, as
  upstream has it. Upstream's foreign-input helper now uses a fresh outpoint,
  because the fork refuses a double spend.
- Swahili: upstream's locale entry is taken as is. seedsigner-translations does
  not ship l10n/sw yet, so it falls back to English until then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
24 new corpus vectors, with _V2 twins where the trap is not version-specific.
derive_followup_vectors.py derives each one from a vendored fixture with a
single edit, and is deterministic:

- OP_RETURN: a Runes runestone (OP_13), small-int pushes and a multi-push are
  negative controls that must be shown. A truncated push and a non-push opcode
  are refused as MALFORMED_OP_RETURN.
- Input scripts: a stray redeem_script, a missing redeem_script or
  witness_script, a wrong redeem_script, and a wrong script on another party's
  input in a collaborative spend.
- Change: nested segwit change without its redeem_script is counted as change.
  A redeem-less output that claims this seed but pays a stranger is refused as
  FORGED_OUTPUT_OWNERSHIP; before the merge it parsed as an ordinary payment.
- A zero fee is accepted.

Also asserts the exact payload shown for each push-only OP_RETURN vector, and
records the vectors in PROVENANCE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tors

psbt_faker now builds every 2026-10-31 follow-up vector itself: same ids,
traps, amounts and verdicts as before. The 24 locally derived fixtures are
replaced with its output, and derive_followup_vectors.py is removed, so the
whole corpus again has one source.

New: four nested segwit (p2sh-p2wsh) multisig vectors at m/48h/0h/0h/1h.
XTRAS.MS_NESTED_HONEST is a negative control.
XTRAS.MS_NESTED_WRONG_WITNESS_SCRIPT breaks only the inner layer
(witness_script to redeem_script) and XTRAS.MS_NESTED_WRONG_REDEEM_SCRIPT
only the outer one (redeem_script to scriptPubKey), so both layers must be
checked. XTRAS.MS_NESTED_MISSING_WITNESS_SCRIPT is MISSING_INPUT_SCRIPT.
dev already refused both layer swaps, so these are regression coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

5 participants