fix: left-pad r/s in Signature.toCompactBytes/toRecoveredBytes - #435
Merged
jxom merged 2 commits intoSep 1, 2026
Merged
Conversation
toCompactBytes and toRecoveredBytes used Bytes.fromHex(v, { size: 32 }),
which right-pads. r/s are big-endian integers, so right-padding a value
shorter than 32 bytes changes its numeric value instead of preserving it
(Signature.from's own normalizer already does this correctly via
Hex.padLeft). A hand-built { r, s, yParity } literal is fully type-legal
(Hex.Hex has no length constraint) and is the natural shape when reading
r/s out of external storage that keeps minimal-form hex.
Over 3000 real secp256k1 signatures, 10 had a leading-zero r byte; of
those, 6 silently recovered the wrong public key through the buggy
round-trip and 4 threw (bad point). Fixed both functions to left-pad via
Hex.padLeft, matching Signature.from's existing pattern.
Also add missing length/yParity preconditions to fromCompactBytes and
fromRecoveredBytes, which previously accepted malformed input silently
via Uint8Array#subarray clamping instead of throwing.
No prior behavioral tests existed for any of these four functions.
|
@gomesalexandre is attempting to deploy a commit to the Wevm Team on Vercel. A member of the Team first needs to authorize it. |
gomesalexandre
marked this pull request as ready for review
September 1, 2026 14:50
commit: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
tl;dr
Signature.toCompactBytesandSignature.toRecoveredBytesright-padr/sto 32 bytes instead of left-padding. Sincer/sare big-endian integers, right-padding a value shorter than 32 bytes changes its numeric value — for a hand-built{ r, s, yParity }literal (fully type-legal, no length constraint onHex.Hex) whoserorshappens to have a leading zero byte, this makesSecp256k1.recoverPublicKeysilently return the wrong public key, with no error, roughly 60% of the time it fires (the rest of the time it throws instead).Where
src/core/Signature.ts:565-574(toCompactBytes) and:623-633(toRecoveredBytes), both viaBytes.fromHex(v, { size: 32 }), which right-pads by construction (src/core/Bytes.ts:223-232, own comment: "Right-pad the hex string before parsing").The same file's own normalizer does this correctly:
Signature.from(:314-317) left-pads viaHex.padLeftafterassert.Reproduction
Over 3000 real secp256k1 signatures (varying the private key against a fixed payload), 10 had a leading-zero
rbyte (~1/256 expected rate). Of those 10, feeding each throughtoRecoveredBytes→fromRecoveredBytes→recoverPublicKeywithrin minimal (non-zero-padded) hex form — the shape you'd get readingrback out of an RPC response, subgraph, or DB that stores minimal-form quantities — produced:Concrete example:
Scope — please read before assuming this is worse than it is
All entry points that construct a
Signaturethrough ox's own APIs (Secp256k1.sign,Signature.from,Signature.fromHex, etc.) already padr/sto 32 bytes before returning, per the design invariant established in #247. I confirmed this myself —Secp256k1.signreturns pre-paddedr/s, so this bug does not fire through the normal signing path. It fires specifically when a caller builds{ r, s, yParity }by hand from a source that storesr/sin minimal (non-zero-padded) hex — no downstream code doing exactly that was found in this repo or observed by me. So: a real, silent, wrong-answer bug on a public, untested API — not "signature verification is broken" in general.Fix
Left-pad instead of right-pad in both functions, matching
Signature.from's existing pattern:Bundled in the same patch (same root cause class — these functions previously accepted malformed input silently):
fromCompactBytes/fromRecoveredBytesusedUint8Array#subarray, which clamps instead of throwing —fromCompactBytes(new Uint8Array(20))previously returned a bogus signature instead of throwing, andfromRecoveredBytesnever validatedyParity ∈ {0,1}. Both now throwSignature.InvalidSerializedSizeError/Signature.InvalidYParityError(reusing the errors the siblingfromHexalready throws for the same conditions).One honest side effect worth flagging: oversized
r/s(>32 bytes) now throwsHex.SizeExceedsPaddingSizeErrorviaHex.padLeftinstead of the previousHex.SizeOverflowErrorviaBytes.fromHex's internalassertSize. Both are real, correctly-typed errors for the same underlying problem; no existing test asserted on the old error type for these two functions (there were zero prior tests for any of the four functions touched here — confirmed via theexportskey-list test, which only listed their names). Happy to adjust if you'd prefer the old error type preserved.receipts
Red-before/green-after confirmed directly: stashed the fix and re-ran
Signature.test.ts— the 6 new tests exercising the bug (padding-direction assertions + the two malformed-input preconditions) failed against unfixed code with the exact wrong values shown above; the other 34 (including the twodefaultround-trip tests that don't exercise the bug) still passed. Restored the fix, all 40 pass.Codex adversarial review didn't return in 5 minutes — killed it and did a thorough self-review instead (that's how the error-type change above got caught and written up honestly rather than glossed over).
risk
Low.
toCompactBytes/toRecoveredBytes/fromCompactBytes/fromRecoveredByteshad no prior test coverage and no internal callers in this repo — the diff only changes the pad direction and adds input validation, both matching patterns already established elsewhere in the same file.