psbt: implement PSBTv2 (BIP-370) support#2496
Conversation
guggero
left a comment
There was a problem hiding this comment.
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.
|
@guggero Thanks for the detailed feedback! I've gone through and addressed all the points you raised: Code style improvements:
Fixed the duplication issue in finalizer.go: Added proper test coverage: The unknowns bug fix: 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! |
46f79e1 to
a91e2e5
Compare
|
Quick follow-up: I've made a few additional refinements for consistency:
The commit history has been cleaned up into 6 logical commits for easier review. Ready for another look when you have time! |
|
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
left a comment
There was a problem hiding this comment.
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,000PSBT_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.
|
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. |
a91e2e5 to
ef4954d
Compare
|
Hi @guggero! I reviewed the Claude code commit and left some code unchanged since it was already correct. 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. 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
left a comment
There was a problem hiding this comment.
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!
|
Hi! @guggero Thanks for the feedback! I've addressed all the suggestions in the latest commit:
All PSBT tests are passing. Ready for your review! |
|
My very old PR that fixes the circular Golang module dependencies got merged. With that, the |
|
@guggero Thanks for the heads-up! I'll look into the module updates and get this rebased right away. |
c18f46d to
e82824e
Compare
|
|
||
| // TxVersion is the PSBT version number. | ||
| // The key is {0x02}. | ||
| // The value is a 32-bit little endian unsigned integer for the version number. |
There was a problem hiding this comment.
The bip outlines that this is a signed integer. https://github.com/bitcoin/bips/blob/master/bip-0370.mediawiki#specification
| // 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 |
There was a problem hiding this comment.
I think it's worth check if we should name this output index because the bip specifies that it's a spent output index.
There was a problem hiding this comment.
yeah sure i will update this to match the descriptive naming used on the bip
| // 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 |
There was a problem hiding this comment.
Shouldn't we be putting this before TimeLocktimeInputType?
| Unknowns []*Unknown | ||
|
|
||
| // Version is the PSBT packet version (0 for BIP-174, 2 for BIP-370). | ||
| Version uint32 |
There was a problem hiding this comment.
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
| return 0, nil | ||
| } | ||
|
|
||
| // 2. Conflict Case: One input requires Time, another requires Height |
There was a problem hiding this comment.
Sanity checking here but the BIP doesn't outline this failure case. Is this how Core implements it?
There was a problem hiding this comment.
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
|
I got through the files 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. |
|
@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 |
|
@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! |
d2f35a3 to
87d6b8c
Compare
guggero
left a comment
There was a problem hiding this comment.
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).
74b6894 to
4d71f42
Compare
guggero
left a comment
There was a problem hiding this comment.
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).
4d71f42 to
f3dc87c
Compare
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
f3dc87c to
a66beb5
Compare
guggero
left a comment
There was a problem hiding this comment.
Thanks for all the updates and the fixed formatting.
A couple of safety issues, otherwise this starts to look pretty good!
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.
a66beb5 to
3aa9695
Compare
Summary
This PR adds full PSBTv2 support to the
btcutil/psbtpackage, 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 versionFallbackLocktime(0x03): Fallback locktimeInputCount(0x04): Number of inputsOutputCount(0x05): Number of outputsTxModifiable(0x06): Modifiability bitfieldNew Per-Input Fields
PreviousTxid(0x0E): Previous TXIDOutputIndex(0x0F): Previous output indexSequence(0x10): Sequence numberTimeLocktime(0x11): Time-based locktimeHeightLocktime(0x12): Height-based locktimeNew Per-Output Fields
Amount(0x03): Output valueScript(0x04): Output scriptPubKeyBIP Compliance
Tests Added
TestPsbtV2LifeCycle: Full create → sign → finalize → extract cycleTestPsbtV2Validation: Invalid V2 packets correctly rejectedTestPsbtV2Counts: InputCount/OutputCount round-tripTestPsbtV2Locktimes: Locktime validation edge casesTest Results
All existing tests continue to pass. No breaking changes to the V0 API.