zpay32: reject duplicate payment hash fields - #11190
yyforyongyu merged 1 commit into
Conversation
🟡 PR Severity: MEDIUM
🟡 Medium (2 files)
🟢 Low (3 files)
AnalysisThe substantive change is confined to To override, add a |
Lrifton92
left a comment
There was a problem hiding this comment.
Reviewed at cd4ec81.
The change does what the description says, and I checked the parts that could go wrong rather than the parts that were claimed.
The duplicate check fires on presence, not on parse success. paymentHashSeen is set at zpay32/decode.go:304 before parse32Bytes runs at :306, so a first p field with an unsupported length (where parse32Bytes returns nil, nil, :454-456) still arms the check and the second p returns ErrDuplicatePaymentHash. That is the case the old code got wrong: the first field was skipped, the second was accepted, and the interpreted hash depended on order. The "malformed then valid" table case and the property test cover exactly that shape.
The error is reachable before the signature is touched. Decode calls parseData at zpay32/decode.go:152, and signature verification / recovery only happens at :189 and :195. So the two hand-built invoices in TestDecodeDuplicatePaymentHashes do not need a valid signature for require.ErrorIs to hold, which is why the test can be that short.
The removed test vector is not a BOLT 11 vector. I grepped the current 11-payment-encoding.md for the lnbc241pveeq09pp5...pp3... string; it is not there, so flipping it from valid to invalid does not contradict a spec example.
Two things, neither blocking.
The comment above parse32Bytes now states the opposite of the spec and of this PR. zpay32/decode.go:452-453 still says "As BOLT-11 states, a reader must skip over the 32-byte fields if it does not have a length of 52". The current reader requirements say a reader MUST fail the payment if any of p, h, s, n does not have the correct length. In practice lnd already fails a lone wrong-length p through validateInvoice (zpay32/invoice.go:386), and this PR fails the wrong-length-then-valid pair, so behaviour is right for p; only the comment is stale. Since this function is what the new check leans on, it seems worth fixing here.
The same order dependence is still there for the other single-occurrence fields. s (zpay32/decode.go:309-321), d, m, n, h, x, c, f and 9 all keep first-seen-wins, and s has the identical "malformed then valid" shape: a first s with the wrong length leaves PaymentAddr unset and the second one is taken. The spec requires exactly one p and exactly one s, and only allows multiples (with preference order) for fields like f, r and b. If the intent is to keep this PR to the payment hash only, a one-line note in the description would save the next reader from asking; otherwise s looks like the natural candidate for the same treatment in a follow-up.
LGTM.
There was a problem hiding this comment.
Overlap with #10326. I have that one open for #9842 / #9718, implementing the reader changes from lightning/bolts#1243 (merged 2025-06-03), which replaced "MUST skip over … p, h or n fields that do NOT have data_lengths of 52, 52 or 53" with "MUST fail the payment if any field with fixed data_length (p, h, s, n) does not have the correct length". It changes parse32Bytes and parseDestination to return ErrInvalidFieldLength, reworks the same case fieldTypeP block this PR touches, and rewrites the same lnbc241pveeq09… vector that's flipped to valid: false here. Whichever lands first, the other needs a rebase — happy to rebase mine on top of this if you'd rather this one go in first.
The two checks compose differently depending on their order. This PR checks for the duplicate before parsing; #10326 parses first and then checks. I combined both locally and decoded the BOLT 11 "fields which must be ignored" example:
- duplicate check first (this PR):
invoice contains multiple payment hashes - parse first (#10326):
payment hash: invalid field length
With the duplicate check first, once the length validation is in, a wrong-length p following a valid one is reported as a duplicate rather than as the length error and ErrInvalidFieldLength becomes unreachable on that path. Parsing first keeps each error pointing at the first problem found. It also makes paymentHashSeen unnecessary, since parse32Bytes no longer returns (nil, nil) — invoice.PaymentHash != nil carries the same information. The "malformed first field followed by a valid one" case from the description still fails either way after #1243, so nothing is lost.
saubyk
left a comment
There was a problem hiding this comment.
Simple but critical change. Just a few observations for consideration.
|
|
||
| // TestDuplicatePaymentHashProperties checks that every pair of valid payment | ||
| // hash fields is rejected, whether the two hashes are identical or distinct. | ||
| func TestDuplicatePaymentHashProperties(t *testing.T) { |
There was a problem hiding this comment.
The drawn bytes cannot influence the outcome: parseTaggedFields returns ErrDuplicatePaymentHash on seeing the second p type before it ever reads the hash contents, so rapid.Check repeats the same two fixed assertions 100 times. The identical/distinct pair is also already covered end-to-end in TestDecodeDuplicatePaymentHashes.
Suggest replacing this with two table rows in TestParseTaggedFields (identical, distinct) and dropping the rapid import from this file.
yyforyongyu
left a comment
There was a problem hiding this comment.
Pending minior release notes, otherwise LGTM!
ziggie1984
left a comment
There was a problem hiding this comment.
LGTM
Pending release notes
|
Seconding @Lrifton92 on the stale comment above // As BOLT-11 states, a reader must skip over the 32-byte fields if
// it does not have a length of 52, so avoid returning an error.That tracks the pre-lightning/bolts#1243 text. The current reader requirements say the opposite:
So the justification the comment gives for Suggested replacement: // A field with an unexpected length is reported as absent rather than
// as an error, leaving it to the caller to decide whether a missing
// field is fatal. Note that BOLT 11 is stricter, and requires a reader
// to fail on a fixed-length field (p, h, s, n) with the wrong length.
// For the payment hash the end result is the same: a lone wrong-length
// field leaves PaymentHash nil and validateInvoice rejects the invoice,
// and a wrong-length field paired with a valid one is rejected as a
// duplicate.Comment-only — behaviour for Separately, on the vectors: I ran lnd's decoder against the 28 vectors proposed in lightning/bolts#1357 ( |
|
Thanks all. Pushed 0a7faa0 addressing the feedback:
Also added a note to the description calling out that the other single-occurrence fields still keep first-seen-wins behavior, with |
0a7faa0 to
b6bd24a
Compare
| the reported network statistics such as total network capacity, channel | ||
| count and max out degree. | ||
|
|
||
| * BOLT 11 invoice decoding [now |
There was a problem hiding this comment.
can you remove this from 22 and fixup this commit maybe ?
saubyk
left a comment
There was a problem hiding this comment.
Only thing outstanding is removing the release note entry for 0.22, since this change is being targeted for 0.21. Other than that it's good to go.
In this commit, we reject BOLT 11 invoices that contain more than one payment hash (`p`) field. `zpay32.Decode` previously kept the first supported payment hash and ignored later fields, so the interpreted payment hash depended on field order. The decoder now tracks whether a `p` field has appeared separately from whether its contents parsed successfully, and any later `p` field returns `ErrDuplicatePaymentHash`. This covers identical hashes, distinct hashes, and a malformed first field followed by a valid one. The change is deliberately stricter than the current BOLT 11 text, which tells a reader to use the first `p` field; it is motivated by lightning/bolts#1357, and will be backported to the v0.20.5 and v0.21.4 releases. We also replace the stale comment above parse32Bytes, which quoted the old BOLT 11 rule that a reader must skip over 32-byte fields with an unsupported length. The current reader requirements say the opposite: a reader must fail the payment if a fixed-length field (p, h, s, n) does not have the correct length. The new comment states that reporting the field as absent is a known divergence, and explains why the end result for the payment hash is the same either way. In the tests, we cover identical and distinct duplicate hashes, a malformed first field followed by a valid one, and the signed vectors. A valid variant of the flipped vector keeps the wrong-length h and n fields, so the coverage that such fields are skipped survives. The duplicate vector in the decode table pins ErrDuplicatePaymentHash through a new wantErr field rather than asserting any decode error.
b6bd24a to
e2f2706
Compare
|
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-11190-to-v0.20.x-branch
git worktree add --checkout .worktree/backport-11190-to-v0.20.x-branch backport-11190-to-v0.20.x-branch
cd .worktree/backport-11190-to-v0.20.x-branch
git reset --hard HEAD^
git cherry-pick -x e2f2706367aa6e7a39f8ee14204b43087e27600f
git push --force-with-lease |
|
Created backport PR for
Please cherry-pick the changes locally and resolve any conflicts. git fetch origin backport-11190-to-v0.21.x-branch
git worktree add --checkout .worktree/backport-11190-to-v0.21.x-branch backport-11190-to-v0.21.x-branch
cd .worktree/backport-11190-to-v0.21.x-branch
git reset --hard HEAD^
git cherry-pick -x e2f2706367aa6e7a39f8ee14204b43087e27600f
git push --force-with-lease |
…20.x-branch [v0.20.x-branch] Backport #11190: zpay32: reject duplicate payment hash fields
…21.x-branch [v0.21.x-branch] Backport #11190: zpay32: reject duplicate payment hash fields
Change Description
In this PR, we reject BOLT 11 invoices that contain more than one payment
hash (
p) field.zpay32.Decodepreviously kept the first supported paymenthash and ignored later fields, so the interpreted payment hash depended on
field order.
The decoder now tracks whether a
pfield has appeared separately from whetherits contents parsed successfully. Any later
pfield returnsErrDuplicatePaymentHash. This covers identical hashes, distinct hashes, and amalformed first field followed by a valid one.
This change is deliberately stricter than the current BOLT 11 text, which
tells a reader to use the first
pfield; it is motivated bylightning/bolts#1357.
Note: the same order dependence remains for the other single-occurrence fields
(
s,d,m,n,h,x,c,f,9), which all keep first-seen-winsbehavior. This PR is intentionally limited to the payment hash; the
s(payment address) field looks like the natural candidate for the same treatment
in a follow-up.
Steps to Test
go test ./zpay32 make lint-nativePull Request Checklist
Testing
Code Style and Documentation
[skip ci]in the commit message for small changes.