blindedpath: avoid uint32 overflow in accumulated fee calc - #11290
allenpiscitello wants to merge 2 commits into
Conversation
225c7c4 to
47217a5
Compare
🟠 PR Severity: HIGH
🟠 High (1 file)
🟢 Low (2 files)
AnalysisThe only non-test, non-docs file changed is To override, add a |
|
The wider intermediates fix the reported overflow around 4,295 msat/ppm. To make the invoice construction robust across the complete valid input range, I think this PR should additionally:
The important invariant is that the fee policy advertised in the invoice must exactly represent the aggregate policies enforced inside the blinded path. If the exact aggregate cannot be encoded in the invoice's I would also expect boundary tests covering:
|
| `uint32` arithmetic, which wrapped once the summed fees exceeded about 4295 | ||
| msat or 4295 ppm. The under-reported fees made payers underpay, so payments | ||
| to such invoices failed. | ||
|
|
There was a problem hiding this comment.
Missing your name in the release notes
|
Thanks, agreed on the invariant. Done in b561474:
Tests: I also updated the release note in bcd9571 to say that such paths are now skipped. |
ziggie1984
left a comment
There was a problem hiding this comment.
Looking good, had some final comments
| ) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("could not calculate blinded path "+ | ||
| "policies: %w", err) |
There was a problem hiding this comment.
Nice that the overflow errors carry the exact aggregate and limit now, but they never reach the logs: the errInvalidBlindedPath branch in BuildBlindedPaymentPaths only logs
Not using route (%s) as a blinded path since it resulted in an invalid blinded path
without err. If all candidates get rejected, the operator only sees could not build any blinded paths with no hint that fees were the reason. Could we include the error in that debug log, e.g. "...invalid blinded path: %v", route, err?
There was a problem hiding this comment.
Done, the debug log in BuildBlindedPaymentPaths now includes the error, so a skipped candidate shows the aggregate and the limit it exceeded.
| ) | ||
| numerator, ok3 := addUint64(baseTerm, totalTerm) | ||
| numerator, ok4 := addUint64(numerator, million-1) | ||
| if !ok1 || !ok2 || !ok3 || !ok4 { |
There was a problem hiding this comment.
Could we use descriptive names for these booleans instead of ok1 through ok4, for example baseTermOK, totalTermOK, termsSumOK, and roundingOK? Each flag maps to a distinct part of the formula, so naming them accordingly would make this combined overflow check easier to audit. The same applies to the fee-rate calculation below.
There was a problem hiding this comment.
Renamed to baseTermOK, totalTermOK, termsSumOK and roundingOK, and to sumTermOK, productTermOK, termsSumOK and roundingOK in the fee-rate calculation.
|
|
||
| # Bug Fixes | ||
|
|
||
| * [Fixed an overflow](https://github.com/lightningnetwork/lnd/pull/11290) |
There was a problem hiding this comment.
Could we clean up the commit history before merging? The implementation and tests are currently split across two commits, while the release-note and contributor changes are spread across three commits. I suggest squashing this into two focused commits: (1) the implementation and its tests, and (2) the release note and contributor entry. That would leave only one commit touching the release notes and make the history easier to follow.
There was a problem hiding this comment.
Squashed into two commits: the fix with its tests, and the release note with the contributor entry.
The aggregate base fee and fee rate advertised in a blinded path's payinfo must exactly represent the policies enforced inside the path. The previous uint32 arithmetic could wrap, valid uint32 fee rates can overflow even a uint64 numerator, the proportional fee was clamped to math.MaxUint32 and the base fee was narrowed with an unchecked uint32 conversion. Each of these silently under-reports the fee the payer must add. Make calcBlindedPathPolicies return an error, compute both aggregates with checked multiplication and addition, and reject an aggregate that does not fit in the invoice's uint32 fields. The error wraps errInvalidBlindedPath, so BuildBlindedPaymentPaths skips only that candidate, logs why, and still builds blinded paths from the remaining usable routes.
bcd9571 to
c17ddea
Compare
Change Description
calcNextTotalBaseFeeandcalcNextTotalFeeRateaccumulate a blinded path's payinfo usinguint32arithmetic (oneMillion = uint32(1_000_000)). Once the summed fee rate passes ~4,295 ppm, or the summed base fee ~4,295 msat, the multiplication by one million wraps and the advertised fees are far lower than the real ones. Payers then underpay, the introduction node still charges its real fee, and the receiver gets an HTLC below the invoice amount that is failed back after the MPP timeout.With the default config (1.1x policy buffer plus one dummy hop at the average policy), a single real hop at 1,100 msat + 2,750 ppm advertises a fee rate of 1,213 ppm instead of 5,508. This has been present since blinded paths were added for receiving (v0.18.3).
The fee policy advertised in the invoice must exactly match the aggregate of the policies enforced inside the blinded path. This change:
uint64with checked multiplication and addition (bits.Mul64/bits.Add64);uint32payinfo fields, instead of clamping or truncating it;calcBlindedPathPoliciesreturn an error wrappingerrInvalidBlindedPath, soBuildBlindedPaymentPathsskips only that candidate route and still builds paths from the other usable routes.Steps to Test
go test ./routing/blindedpath/ -run 'TestBlindedPathAccumulatedPolicyCalc|TestBuildBlindedPathSkipsFeeOverflow'TestBlindedPathAccumulatedPolicyCalcLargeFeescovers the original wrap, which fails on master (1,213 instead of 5,508 ppm, 1,412 instead of 10,001 msat). For both the base fee and the fee rate it also covers an aggregate of exactlymath.MaxUint32(accepted),math.MaxUint32 + 1(rejected) and auint64intermediate overflow (rejected).TestBuildBlindedPathSkipsFeeOverflowchecks that a candidate whose fees can't be represented is skipped while a valid candidate is still returned.Pull Request Checklist
Testing
Code Style and Documentation
[skip ci]in the commit message for small changes.📝 Please see our Contribution Guidelines for further guidance.