Skip to content

PSBTv2 (BIP-370) Support#87

Open
notTanveer wants to merge 28 commits into
diybitcoinhardware:masterfrom
notTanveer:bip-370
Open

PSBTv2 (BIP-370) Support#87
notTanveer wants to merge 28 commits into
diybitcoinhardware:masterfrom
notTanveer:bip-370

Conversation

@notTanveer
Copy link
Copy Markdown

@notTanveer notTanveer commented Jun 15, 2025

This PR improves PSBT (Partially Signed Bitcoin Transaction) v2 support, especially strict validation and compatibility with BIP 370 test vectors. It also adds a comprehensive set of tests for PSBTv2 edge cases and locktime determination.


Details

1. PSBTv2 Compliance and Parsing Improvements

  • Updated method signatures in src/embit/liquid/pset.py to allow passing the version parameter down to read_value and its parent implementations. This ensures correct handling of versioned PSBT fields.
  • Adjusted calls to super().read_value to pass along the version argument, improving extensibility and correctness for PSBTv2.

2. PSBT View Updates

  • Modified src/embit/psbtview.py:
    • The input() and output() methods now pass the version parameter when reading from the stream, allowing PSBT-in and out scopes to parse versioned fields correctly.

3. Comprehensive PSBTv2 Test Vectors

  • Added a new test file: tests/tests/test_psbtV2.py containing:
    • Test vectors based on BIP 370 PSBTv2.
    • Tests for invalid PSBTv0/PSBTv2 edge cases (e.g., wrong/missing fields, invalid locktimes).
    • Tests for valid PSBTv2 minimal and full-featured transactions.
    • Tests for locktime determination logic in PSBTv2, including edge cases with multiple locktimes (height and time).

Motivation

  • Ensures full BIP 370 compliance, increasing interoperability and correctness for PSBTv2 parsing and validation.
  • Catches subtle bugs and regressions through comprehensive test coverage.
  • Lays the groundwork for future PSBTv2 features and broader Elements/Liquid support.

Testing

  • Run pytest tests/tests/test_psbtV2.py to verify all BIP 370 test vectors and locktime cases.
  • Built from the referenced PR and generated various PSBTs, then imported them into embit to confirm correct parsing — everything worked as expected.
  • Would love for others to build Bitcoin Core + CLI from PR #21283, try it out, and share your observations here.

References

Comment thread src/embit/psbt.py Outdated
@notTanveer notTanveer marked this pull request as ready for review June 22, 2025 11:30
@notTanveer notTanveer changed the title Add PSBTv2 (BIP-370) Support to Embit PSBTv2 (BIP-370) Support Jul 24, 2025
pythcoiner and others added 21 commits March 17, 2026 11:00
Add a check for `p2sh-p2wsh`, `p2sh-p2wpkh` and raise an exception for
unknow types.

fix diybitcoinhardware#93

Co-authored-by: edilmedeiros <jose.edil@gmail.com>
Co-authored-by: moisespompilio <93723302+moisesPompilio@users.noreply.github.com>
In order to test the replace assertions with conditional error raising,
specifically, in bip85 code, was added a test for `derive_entropy`
method as well the failure cases described in `derive_mnemonic` and
`derive_hex`.
…rsing

- Add LOCKTIME_THRESHOLD constant (500000000) for locktime type discrimination
- Add required_time_locktime and required_height_locktime fields to InputScope
- Propagate those fields in InputScope.update()
- Parse PSBT_IN_REQUIRED_TIME_LOCKTIME (0x11) and PSBT_IN_REQUIRED_HEIGHT_LOCKTIME
  (0x12) in InputScope.read_value(), with range validation
- Serialize 0x11 and 0x12 in InputScope.write_to() under the version==2 block
- Add version=None parameter to InputScope.read_value() and OutputScope.read_value()
  so callers can signal the PSBT version being parsed
- Add InputScope.read_from() and OutputScope.read_from() classmethods that accept
  and forward the version parameter (supersede PSBTScope base classmethod for v2)
- Propagate version in LInputScope.read_value() and LOutputScope.read_value()
  (pset.py) so the Liquid subclasses stay compatible
- Pass version=self.version when PSBTView calls read_from() for input/output scopes
  so PSBTv2 scopes are parsed with the correct version context
@notTanveer notTanveer force-pushed the bip-370 branch 5 times, most recently from 01b834d to c001bb1 Compare May 11, 2026 19:54
- Add TxModifiable class with INPUTS/OUTPUTS/SIGHASH_SINGLE bit flags
- Add PSBT._validate_v2_output() classmethod to check required PSBTv2 output
  fields; PSET overrides it to allow value_commitment in place of value
- Add tx_modifiable_flags field to PSBT.__init__() (None for v0/unset)
- Update comment on self.version to note '2 for v2'
- Add PSBT.determine_locktime(): implements BIP-370 locktime selection
  algorithm (height vs time preference, max-of-minimums)
- Update PSBT.tx property to call determine_locktime() for PSBTv2
- Update PSBT.write_to(): serialize PSBT_GLOBAL_TX_MODIFIABLE (0x06) when set
- Rewrite PSBT.read_from(): collect all global KVs into OrderedDict before
  branching on PSBTv2 vs PSBTv0; detect version from 0xfb; reject unsigned
  tx in PSBTv2 and reject v2-only keys in PSBTv0; validate required fields
  after parsing; call read_from with version=version on all scopes
- Rewrite PSBT.parse_unknowns(): strip 0xfb/0x06; guard 0x02/0x03/0x04/0x05
  behind version==2; track _raw_input_count_from_global and
  _raw_output_count_from_global for PSBTv2 scope parsing
- Add version guards to InputScope.read_value() for 0x0e/0x0f/0x10:
  PSBT_IN_PREVIOUS_TXID, PSBT_IN_OUTPUT_INDEX, and PSBT_IN_SEQUENCE are
  rejected when version != 2
- Add version guards to OutputScope.read_value() for 0x03/0x04:
  PSBT_OUT_AMOUNT and PSBT_OUT_SCRIPT are rejected when version != 2
…_with

- Update sign_with(): after each legacy/segwit signature, update tx_modifiable_flags
  per BIP-370: clear INPUTS bit if not ANYONECANPAY, clear OUTPUTS bit if not NONE,
  set SIGHASH_SINGLE bit if sighash type is SINGLE
- Add get_tx_modifiable() / set_tx_modifiable(flags): getter/setter for
  PSBT_GLOBAL_TX_MODIFIABLE; setter raises PSBTError on PSBTv0
- Add is_inputs_modifiable(): True if version!=2, tx_modifiable_flags is None, or
  INPUTS bit is set
- Add is_outputs_modifiable(): True if version!=2, tx_modifiable_flags is None, or
  OUTPUTS bit is set
- Add has_sighash_single(): True only if version==2, tx_modifiable_flags is not None,
  and SIGHASH_SINGLE bit is set
- Add add_input(input_scope): appends an InputScope, gated on is_inputs_modifiable();
  updates _raw_input_count_from_global for PSBTv2
- Add add_output(output_scope): appends an OutputScope, gated on is_outputs_modifiable();
  updates _raw_output_count_from_global for PSBTv2
Add test_psbtV2.py with 37 tests covering PSBTv2 parsing, serialization,
locktime selection, TxModifiable flags, and add_input/add_output API.
@odudex odudex changed the base branch from develop to master May 12, 2026 12:10
@odudex odudex requested review from miketlk and odudex as code owners May 12, 2026 12:10
Copy link
Copy Markdown
Collaborator

@odudex odudex left a comment

Choose a reason for hiding this comment

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

Please rebase with master (we plan to stop using develop branch). There shouldn't be conflicts rebasing it locally with git.

Later, there are a few things to take a look at:

  1. PSBTView does not fully enforce or apply PSBTv2 rules. It can accept a v2 PSBT with PSBT_GLOBAL_UNSIGNED_TX if that key appears before PSBT_GLOBAL_VERSION, and PSBTView.locktime uses only fallback locktime instead of deriving locktime from per-input requirements. Sighashes computed through PSBTView can therefore commit to the wrong locktime.

  2. add_input() / add_output() only check modifiable flags. BIP370 Constructor rules also require validating required fields, locktime compatibility, preserving existing signed locktime, and handling SIGHASH_SINGLE input/output pairing.

  3. Several PSBTv2 fixed-width input fields lack exact length validation: previous txid, output index, sequence, required time locktime, and required height locktime. Malformed values can be accepted and later serialize incorrectly or fail unexpectedly.

  4. compress mode can lose its OOM-protection benefit for PSBTv2 non-witness UTXOs when PSBT_IN_NON_WITNESS_UTXO appears before PSBT_IN_PREVIOUS_TXID / PSBT_IN_OUTPUT_INDEX in the input map. Since key order is not guaranteed, this should be pre-scanned, deferred, or documented.

  5. Signedness should be made consistent with BIP370: PSBT_OUT_AMOUNT is read as signed but written unsigned, and PSBT_GLOBAL_TX_VERSION is specified as signed int32 but read/written unsigned.

  6. Explicit PSBTv0 version handling is too strict. BIP174 allows v0 PSBTs to either omit PSBT_GLOBAL_VERSION or set it to 0, but this parser rejects version 0.

  7. PSBTv2 global input/output counts are used to allocate placeholder scope objects before parsing actual scopes. Since those counts are attacker-controlled and the placeholders are later discarded, this creates an avoidable memory-exhaustion risk. Store counts only and allocate scopes as they are parsed.

Recommended test additions: PSBTv2 parse/serialize roundtrips, PSBTView locktime/sighash behavior, constructor API behavior, signer TX_MODIFIABLE updates, malformed fixed-width field lengths, and the PSET v2 output validation path.

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.

7 participants