Skip to content

psbt: implement PSBTv2 (BIP-370) support#2496

Open
TechLateef wants to merge 6 commits into
btcsuite:masterfrom
TechLateef:psbtv2-implementation
Open

psbt: implement PSBTv2 (BIP-370) support#2496
TechLateef wants to merge 6 commits into
btcsuite:masterfrom
TechLateef:psbtv2-implementation

Conversation

@TechLateef

Copy link
Copy Markdown

Summary

This PR adds full PSBTv2 support to the btcutil/psbt package, implementing BIP-370.

Fixes #2328.

This is also a prerequisite for full BIP-375 compliance in PR #2244 (Silent Payments), as noted by @benma.

Changes

New Global Fields (PSBTv2)

  • TxVersion (0x02): Transaction version
  • FallbackLocktime (0x03): Fallback locktime
  • InputCount (0x04): Number of inputs
  • OutputCount (0x05): Number of outputs
  • TxModifiable (0x06): Modifiability bitfield

New Per-Input Fields

  • PreviousTxid (0x0E): Previous TXID
  • OutputIndex (0x0F): Previous output index
  • Sequence (0x10): Sequence number
  • TimeLocktime (0x11): Time-based locktime
  • HeightLocktime (0x12): Height-based locktime

New Per-Output Fields

  • Amount (0x03): Output value
  • Script (0x04): Output scriptPubKey

BIP Compliance

  • Key-value pairs serialized in strictly increasing numerical order (BIP-174)
  • V2 required fields preserved after finalization (BIP-370)
  • Mixed locktime types (time + height) correctly rejected with an error
  • Unknown field duplicate detection checks key-only

Tests Added

  • TestPsbtV2LifeCycle: Full create → sign → finalize → extract cycle
  • TestPsbtV2Validation: Invalid V2 packets correctly rejected
  • TestPsbtV2Counts: InputCount/OutputCount round-trip
  • TestPsbtV2Locktimes: Locktime validation edge cases

Test Results

All existing tests continue to pass. No breaking changes to the V0 API.

ok  github.com/btcsuite/btcd/btcutil/psbt  0.009s

@TechLateef

TechLateef commented Mar 16, 2026

Copy link
Copy Markdown
Author

@guggero kindly take a look when you have a moment. This implements the BIP-370 logic required for the Silent Payments PR (#2244) mentioned by @benma. All existing V0 tests pass, and I've added comprehensive V2 lifecycle tests.

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the PR! Code looks pretty good, but I think there are probably a few hidden edge cases that need to be ironed out.

It's a lot of code in a single commit, so not very easy to go through. If you want to increase your changes of finding a second reviewer, I suggest to split things up where possible.

I did a first pass, will probably require a few more rounds.

Comment thread btcutil/psbt/creator.go Outdated
Comment thread btcutil/psbt/finalizer.go Outdated
Comment thread btcutil/psbt/partial_input.go Outdated
Comment thread btcutil/psbt/partial_input.go Outdated
Comment thread btcutil/psbt/partial_input.go
Comment thread btcutil/psbt/psbt.go Outdated
Comment thread btcutil/psbt/types.go Outdated
Comment thread btcutil/psbt/types.go Outdated
Comment thread btcutil/psbt/types.go Outdated
Comment thread psbt/updater.go
@TechLateef

Copy link
Copy Markdown
Author

@guggero Thanks for the detailed feedback! I've gone through and addressed all the points you raised:

Code style improvements:

  • Switched all the version checks to use switch statements instead of if/else - much cleaner and follows the Go happy path pattern
  • Fixed the formatting to match what's already used in PInput.serialize() - applied the multi-line style consistently

Fixed the duplication issue in finalizer.go:
This was a good catch. That same PSBTv2 field copying block was repeated 3 times, which would definitely be a maintenance headache. I created a CopyInputFields() method that handles all the field copying + the unknowns bug you mentioned. Now it's just one method call instead of all that repeated code.

Added proper test coverage:
Added comprehensive tests for the DetermineLockTime algorithm covering the BIP-370 requirements, including the height-preference tie-breaker and conflict detection. Also tested the unknown field handling.

The unknowns bug fix:
You were right that NewPsbtInput was dropping unknown fields during finalization. The new method does a proper deep copy to avoid any shared memory issues.

I think this addresses all the main concerns. The code is much cleaner now and should be easier to maintain going forward. Let me know if there's anything else that needs attention!

@TechLateef TechLateef requested a review from guggero March 19, 2026 23:15
@TechLateef TechLateef force-pushed the psbtv2-implementation branch 3 times, most recently from 46f79e1 to a91e2e5 Compare March 21, 2026 14:13
@TechLateef

Copy link
Copy Markdown
Author

Quick follow-up: I've made a few additional refinements for consistency:

  • Converted remaining if/else version checks to switch statements throughout the codebase
  • Ensured consistent code style across all files
  • All PSBTv2 version handling now follows the same clean switch pattern

The commit history has been cleaned up into 6 logical commits for easier review. Ready for another look when you have time!

@guggero

guggero commented Mar 25, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the updates and the reworked commit structure! Will be useful when re-reviewing. Will take a look as soon as I find some time.

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I tasked Claude Code with a review, here's the result. You can find the commit with the fixed code and the additional test cases here: guggero@ef41278

You can use from that what you want, just please clean it up first, as it's agent-generated code.

Claude Code PR Review

Critical Bugs (FIXED)

1. SumUtxoInputValues panics on PSBTv2 (utils.go:297-335)

SumUtxoInputValues unconditionally dereferences packet.UnsignedTx.TxIn, which
is nil for v2 PSBTs. This causes a nil pointer panic. GetTxFee() calls this
function so it also crashes on v2.

Fix applied: Check packet.Version and use packet.Inputs / pInput.OutputIndex
for v2 instead of packet.UnsignedTx.TxIn. Tests: TestV2SumUtxoInputValues*,
TestV2GetTxFee.

2. Input serialization key ordering violates BIP-174 (partial_input.go)

BIP-174 requires key-value pairs in ascending key order. For unfinalized v2
inputs, the v2 fields (0x0e-0x12) were serialized after taproot fields
(0x13-0x18), violating the ordering requirement (0x0e < 0x13).

Fix applied: Split the non-finalized input block so that 0x02-0x06 are written
first, then 0x07-0x08 (FinalScriptSig/Witness), then v2 fields 0x0e-0x12, then
taproot fields 0x13-0x18. Test: TestV2InputSerializationKeyOrder.

Spec Compliance Issues (FIXED)

3. Missing locktime value validation (partial_input.go)

BIP-370 mandates:

  • PSBT_IN_REQUIRED_TIME_LOCKTIME: value must be >= 500,000,000
  • PSBT_IN_REQUIRED_HEIGHT_LOCKTIME: value must be > 0 and < 500,000,000

The deserialization code accepted any uint32 value without validation.

Fix applied: Added range checks after reading the values. Tests:
TestV2TimeLocktimeMustBeGTE500M, TestV2TimeLocktimeBoundary,
TestV2HeightLocktimeMustBeGTZeroAndLT500M, TestV2HeightLocktimeValidBoundary.

4. No duplicate detection for v2 input fields (partial_input.go)

OutputIndex, TimeLocktime, HeightLocktime, and Sequence had no duplicate
key detection.

Fix applied: Added outputIndexSeen, sequenceSeen, timeLocktimeSeen,
heightLockSeen booleans. Tests: TestV2DuplicateInput*.

5. FallbackLocktime and TxModifiable duplicate detection (psbt.go)

These used if value != 0 for duplicate detection instead of dedicated booleans
like txVersionSeen.

Fix applied: Added fallbackLocktimeSeen and txModifiableSeen booleans.
Tests: TestV2DuplicateGlobalFallbackLocktime, TestV2DuplicateGlobalTxModifiable.

6. PSBT_OUT_AMOUNT type: signed vs unsigned (partial_output.go)

BIP-370 specifies a signed 64-bit integer. The struct used uint64.

Fix applied: Changed POutput.Amount from uint64 to int64 throughout,
matching wire.TxOut.Value. Test: TestV2AmountSignedInt64.

Design Issues (Not Fixed)

7. V2-specific fields not rejected in v0 PSBTs

BIP-370 says input fields 0x0e-0x12 and output fields 0x03-0x04 must be excluded
from v0. Currently they are silently parsed. A strict implementation would route
them to the unknown list for v0. This is left as-is for now since the version is
not yet known at per-input/per-output parsing time (it's a global field that comes
before the inputs/outputs).

8. NewV2 doesn't validate TxVersion (creator.go:72)

New() checks version >= MinTxVersion but NewV2() accepts any value. BIP-370
says "the transaction version number must be set to at least 2" when others will
add inputs/outputs.

9. AddInputV2/AddOutputV2 don't check TxModifiable flags

BIP-370 says the Constructor must check PSBT_GLOBAL_TX_MODIFIABLE before adding
inputs/outputs. The library doesn't enforce this, leaving it to callers.

10. Optional fields always serialized (psbt.go:666-700)

FallbackLocktime and TxModifiable are always written for v2, even when 0. Per
BIP-370 these are optional with a default of 0. Writing them is valid but verbose.

11. Silent TxVersion upgrade (psbt.go:567-569)

If txVersion=0 is explicitly present in a v2 PSBT, it's silently upgraded to 2.
This could mask invalid input data.

@TechLateef

Copy link
Copy Markdown
Author

Appreciate the detailed review. I’m already on it I'll clean up the generated code, verify the fixes thoroughly, and make a proper push once everything checks out.

@TechLateef TechLateef force-pushed the psbtv2-implementation branch from a91e2e5 to ef4954d Compare April 12, 2026 01:40
@TechLateef TechLateef requested a review from guggero April 12, 2026 01:41
@TechLateef

Copy link
Copy Markdown
Author

Hi @guggero! I reviewed the Claude code commit and left some code unchanged since it was already correct.
Regarding the TxModifiable check I intentionally kept the Creator's AddInputV2 and AddOutputV2 without the flag check, because per BIP-370 the Creator is the one responsible for setting TxModifiable, not checking it.

Also, BIP-370 explicitly states that TxModifiable may be omitted entirely when the Creator is also the Constructor, so adding the check there would incorrectly reject valid fresh PSBTs where TxModifiable is 0 or not yet set.
Instead, I added AddInputV2 and AddOutputV2 to the Updater, which is consistent with how this codebase already combines the Constructor and Updater roles.

The new methods enforce the appropriate PSBT_GLOBAL_TX_MODIFIABLE flag checks per BIP-370 bit 0 (&1) for inputs and bit 1 (&2) for outputs so that inputs and outputs can only be added when the flags explicitly permit it.

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A few nits to fix readability and code consistency, but other than those this looks good to me. Thanks a lot for your work on this!

Comment thread btcutil/psbt/types.go Outdated
Comment thread btcutil/psbt/psbt.go Outdated
Comment thread psbt/psbt.go Outdated
Comment thread btcutil/psbt/psbt.go Outdated
Comment thread btcutil/psbt/psbt.go Outdated
Comment thread psbt/psbt.go
Comment thread btcutil/psbt/psbt.go Outdated
Comment thread btcutil/psbt/psbt.go Outdated
Comment thread btcutil/psbt/partial_input.go Outdated
Comment thread btcutil/psbt/finalizer.go Outdated
@TechLateef

Copy link
Copy Markdown
Author

Hi! @guggero Thanks for the feedback! I've addressed all the suggestions in the latest commit:

  • Refactoring: Replaced isSaneKey with explicit checks and inlined addUnknownField to remove the pointer-to-slice logic.
  • Validation: Moved the PSBTv2 version check into the switch version block and added it to SanityCheck for global enforcement.
  • Idiomatic Go: Updated loops to use range, simplified txIn initialization, and swapped manual deep copies for bytes.Clone.
  • Cleanup: Combined local variables into var blocks and consolidated version-specific logic.

All PSBT tests are passing. Ready for your review!

@TechLateef TechLateef requested a review from guggero April 22, 2026 16:15
@guggero

guggero commented May 20, 2026

Copy link
Copy Markdown
Collaborator

My very old PR that fixes the circular Golang module dependencies got merged. With that, the psbt/v2 module is now outside of the btcutil module. Therefore this requires a rebase.

@TechLateef

TechLateef commented May 20, 2026

Copy link
Copy Markdown
Author

@guggero Thanks for the heads-up! I'll look into the module updates and get this rebased right away.

@TechLateef TechLateef force-pushed the psbtv2-implementation branch from c18f46d to e82824e Compare May 20, 2026 23:00

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-ACK 🎉

Comment thread psbt/types.go Outdated

// TxVersion is the PSBT version number.
// The key is {0x02}.
// The value is a 32-bit little endian unsigned integer for the version number.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Comment thread psbt/types.go Outdated
// OutputIndex is the output index of the previous transaction.
// The key is {0x0F}.
// The value is a 32-bit little endian unsigned integer.
OutputIndexInputType InputType = 0x0F

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it's worth check if we should name this output index because the bip specifies that it's a spent output index.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

yeah sure i will update this to match the descriptive naming used on the bip

Comment thread psbt/types.go Outdated
// Sequence is the sequence number for this input.
// The key is {0x10}.
// The value is a 32-bit little endian unsigned integer.
SequenceInputType InputType = 0x10

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Shouldn't we be putting this before TimeLocktimeInputType?

Comment thread psbt/psbt.go
Unknowns []*Unknown

// Version is the PSBT packet version (0 for BIP-174, 2 for BIP-370).
Version uint32

@kcalvinalvin kcalvinalvin Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yeah ok clearly wrong here. Should be int32

EDIT: ok so it's not this version. It's the psbtv2 version that should be int32

Comment thread psbt/psbt.go
return 0, nil
}

// 2. Conflict Case: One input requires Time, another requires Height

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sanity checking here but the BIP doesn't outline this failure case. Is this how Core implements it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Actually, this exact failure case is outlined at the very end of the BIP 370 spec under the 'Test Vectors / Determining Lock Time' section.

The timelock for the following PSBTs cannot be computed: Case: Input 1 has PSBT_IN_REQUIRED_HEIGHT_LOCKTIME of 10000, Input 2 has PSBT_IN_REQUIRED_TIME_LOCKTIME of 1657048460

the data format error might be wrong using ErrInvalidPsbtFormat

i will update it to return something like ErrLockTimeConflict

@kcalvinalvin

Copy link
Copy Markdown
Collaborator

I got through the files types.go and psbt.go. I don't think this PR is close to a mergeable state based on the small bugs I've found. Besides the bugs, there's a bit of design considerations that I think should be taken into account for the sake of future maintainability.

Also, I think this PR was done by claude or some other ai tool. I'm not against ai tools but could manually reviewing the results is a minimum imo.

@TechLateef

Copy link
Copy Markdown
Author

@kcalvinalvin Thanks for the review i saw all the comments and feedbacks you drop and the PR is not actually done by AI tool i do actually use it to break down complex parts of the BIP specs and also i did create the implementation on my person repo before creating this pr

@TechLateef

Copy link
Copy Markdown
Author

@kcalvinalvin I'm going to convert this PR to a Draft for now so I can go back through the BIP, redesign the structure, and make sure it’s fully maintainable. I’ll mark it ready for review again once the cleanups are done!
cc: @guggero @saubyk

@TechLateef TechLateef marked this pull request as draft June 5, 2026 14:08
@TechLateef TechLateef force-pushed the psbtv2-implementation branch 6 times, most recently from d2f35a3 to 87d6b8c Compare June 13, 2026 10:52
@TechLateef TechLateef marked this pull request as ready for review June 13, 2026 10:55

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I did another pass on this. We're definitely getting closer, thanks a lot for the previous edits.
I have several suggestions to make the code even more readable and reviewer-friendly.
Please make sure to apply each comment to the whole PR and not just the single place where the comment is pinned (to avoid repeating myself too often).

Comment thread psbt/types.go
Comment thread psbt/types.go Outdated
Comment thread psbt/psbt.go Outdated
Comment thread psbt/psbt.go Outdated
Comment thread psbt/psbt.go Outdated
Comment thread psbt/partial_output.go Outdated
Comment thread psbt/types.go Outdated
Comment thread psbt/partial_input.go
Comment thread psbt/signer.go Outdated
Comment thread psbt/psbt_test.go
@TechLateef TechLateef force-pushed the psbtv2-implementation branch 2 times, most recently from 74b6894 to 4d71f42 Compare June 18, 2026 11:13
@TechLateef TechLateef requested a review from guggero June 18, 2026 11:19

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please address all of my previous feedback across all commits before re-requesting review. Just a few examples here, not the full list (self-review each commit before pushing please).

Comment thread psbt/psbt.go Outdated
Comment thread psbt/psbt.go Outdated
Comment thread psbt/psbt.go Outdated
Comment thread psbt/creator.go Outdated
@TechLateef TechLateef force-pushed the psbtv2-implementation branch from 4d71f42 to f3dc87c Compare June 19, 2026 13:21
Add the fundamental type definitions for PSBTv2 as specified in BIP-370:
- Global fields: TxVersion, FallbackLocktime, InputCount, OutputCount, TxModifiable
- Input fields: PreviousTxid, OutputIndex, Sequence, TimeLocktime, HeightLocktime
- Output fields: Amount, Script
@TechLateef TechLateef force-pushed the psbtv2-implementation branch from f3dc87c to a66beb5 Compare June 20, 2026 08:10
@TechLateef TechLateef requested a review from guggero June 20, 2026 18:08

@guggero guggero left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for all the updates and the fixed formatting.
A couple of safety issues, otherwise this starts to look pretty good!

Comment thread psbt/psbt.go
Comment thread psbt/partial_input.go
Comment thread psbt/psbt.go
Comment thread psbt/signer.go
Comment thread psbt/signer.go
Comment thread psbt/updater.go
Comment thread psbt/updater.go
Add comprehensive PSBTv2 field management for inputs and outputs:
- PSBTv2 input fields: PreviousTxid, OutputIndex, Sequence, Locktimes
- PSBTv2 output fields: Amount, Script
- Proper field serialization with BIP-174 ordering
- Field copying and preservation during finalization
- Unknown field handling with duplicate detection
Implement the core PSBTv2 packet handling including:
- PSBTv2 serialization with proper field ordering
- BIP-370 lock time determination algorithm
- Version detection and validation
- Unknown field handling with duplicate detection
Add PSBTv2 support to the complete PSBT workflow pipeline:
- PSBTv2 packet creation with proper initialization
- Update operations with version-aware field handling
- Signing operations with PSBTv2 compatibility
- Transaction extraction with version detection
- Future-proof version check patterns
Add PSBTv2-aware finalization logic:
- Preserve required PSBTv2 fields during finalization
- Maintain unknown fields as mandated by BIP-370
- Proper field copying with CopyInputFields method
Remove legacy malformed/truncated test vectors (cases 5 and 6) that correctly fail under the new strict BIP-370 parsing rules. The new strict parser interprets unexpected io.EOF as an error.
@TechLateef TechLateef force-pushed the psbtv2-implementation branch from a66beb5 to 3aa9695 Compare June 25, 2026 12:15
@TechLateef TechLateef requested a review from guggero June 25, 2026 12:20
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.

bip-0370 PSBT Version 2 support

3 participants