Skip to content

PSBTv2 (BIP-370) Support#87

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

PSBTv2 (BIP-370) Support#87
notTanveer wants to merge 8 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
@notTanveer notTanveer force-pushed the bip-370 branch 6 times, most recently from c001bb1 to 64a2ae2 Compare May 11, 2026 20:40
@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.

Copilot AI review requested due to automatic review settings May 13, 2026 18:10
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR expands PSBTv2/BIP-370 support across parsing, serialization, signing metadata, lightweight PSBT views, and Liquid/PSET compatibility, with a new suite of PSBTv2 test vectors.

Changes:

  • Adds PSBTv2 required-field validation, locktime determination, constructor helpers, and tx-modifiable flag handling.
  • Updates PSBTView and Liquid PSET parsing to pass version-aware scope parsing through inputs/outputs.
  • Adds extensive PSBTv2 validity, locktime, constructor, and signer-flag tests.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/embit/psbt.py Implements PSBTv2 fields, validation, locktime logic, constructor helpers, and signing flag updates.
src/embit/psbtview.py Adds PSBTv2-aware view parsing, locktime/tx-version handling, and global tx-modifiable rewriting.
src/embit/liquid/pset.py Propagates PSBT version into Liquid scope parsing and adjusts PSET v2 output validation.
tests/tests/test_psbtV2.py Adds BIP-370 test vectors and coverage for PSBTv2 parsing, locktime, construction, and signer behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/embit/psbt.py Outdated
Comment thread src/embit/psbt.py
Comment thread src/embit/psbtview.py
Comment thread tests/tests/test_psbtV2.py
@notTanveer notTanveer requested a review from odudex May 14, 2026 08:18
@odudex
Copy link
Copy Markdown
Collaborator

odudex commented May 14, 2026

I sent you a PR with final nits and adaptations for older MicroPython versions, like the one we run in Krux.
I did very basic tests, and with the commits I sent you, is works with Krux.

Please evaluate the commits and merge them if you agree with changes. Let me know otherwise

odudex
odudex previously approved these changes May 14, 2026
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.

tACK

I did basic testing with Krux, though nothing specific to the newly added PSBTv2 features. Basic functionality worked after replacing a few syntax constructs with simpler equivalents compatible with Krux’s older MicroPython version.

qlrd

This comment was marked as duplicate.

qlrd

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@qlrd qlrd left a comment

Choose a reason for hiding this comment

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

tACK, just nit:

Comment thread src/embit/psbt.py
…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
- 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 48 tests covering PSBTv2 parsing, serialization,
locktime selection, TxModifiable flags, and add_input/add_output API.
odudex added 3 commits May 16, 2026 02:20
- reject BIP370-only globals in PSBTView v0 parsing
- preserve signed and zero tx versions when synthesizing transactions
- serialize transaction versions as signed int32
- keep PSBTv2 compressed non-witness UTXO parsing key-order independent
- add regression coverage for the follow-up findings
Replace new PSBTv2/PSET f-strings with old-style formatting so the
branch remains parseable on older MicroPython versions.
Replace signed=True usage in BIP370 signed integer parsing and serialization with helper functions compatible with older MicroPython byte conversion APIs.

This keeps PSBTv2 transaction versions and output amounts spec-compliant without changing regular keyword-argument call sites.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/embit/psbtview.py
h = hashes.tagged_hash_init("TapSighash", b"\x00")
h.update(bytes([sighash]))
h.update(self.tx_version.to_bytes(4, "little"))
h.update(_signed_to_bytes(self.tx_version, 4))
Comment thread src/embit/psbtview.py
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.

6 participants