fix(multisig): reject non-adjacent duplicate signers in EcdsaSignerManager - #676
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| const aHigh = slice<16>(a, 16) as Uint<128>; | ||
| const bLow = slice<16>(b, 0) as Uint<128>; | ||
| const bHigh = slice<16>(b, 16) as Uint<128>; | ||
| return (aHigh > bHigh) || ((aHigh == bHigh) && (aLow > bLow)); |
There was a problem hiding this comment.
🔵 followup: The low-limb branch (aHigh == bHigh && aLow > bLow) and the little-endian Bytes<16> as Uint<128> assumption it rests on are not exercised — all test commitments are persistentHash outputs whose high limbs differ, so this branch never runs. Distinctness (the #629 fix) holds regardless of endianness, but if the cast were not LSB-first the shipped sortByCommitmentField would mis-order callers and reject honest n>=3 quorums (liveness, not a security break). Add a unit test on crafted 32-byte values: a pair equal in bytes 16–31 differing only in bytes 0–15, and a pair crossing the limb boundary; assert the TS sort order matches on-chain accept/reject.
added by claude (dev3-midnight-basic-review)
| it('should reject an unregistered signer', async () => { | ||
| const { pubkeys, signatures } = sortedPair(PK_UNKNOWN, PK2); | ||
| // Order may place unknown first or second; either fails membership or order | ||
| await expect(verifier.verify(MSG, pubkeys, signatures)).rejects.toThrow(); |
There was a problem hiding this comment.
⚪ nitpick: This lost its specific-message assertion because sort order decides whether the unknown key fails on membership or on ordering. Make it deterministic — present a registered key that sorts before the unknown one — so you can assert 'SignerManager: not a signer' again instead of "any throw".
added by claude (dev3-midnight-basic-review)
| const bHigh = slice<16>(b, 16) as Uint<128>; | ||
| return (aHigh > bHigh) || ((aHigh == bHigh) && (aLow > bLow)); | ||
| } | ||
| } |
There was a problem hiding this comment.
⚪ nitpick (if-minor): Stray trailing blank line after the module closing brace; compact fmt / biome will likely flag it.
added by claude (dev3-midnight-basic-review)
Co-authored-by: 0xisk <iskander.andrews@openzeppelin.com> Co-authored-by: 0xisk <0xisk@proton.me>
Co-authored-by: 0xisk <iskander.andrews@openzeppelin.com> Co-authored-by: 0xisk <0xisk@proton.me>
Co-authored-by: 0xisk <iskander.andrews@openzeppelin.com> Co-authored-by: 0xisk <0xisk@proton.me>
…nager verify only compared each commitment to the previous one, so [A,B,A] counted A twice toward the threshold when n >= 3. Require strictly increasing commitments under Compact Bytes→integer order (first byte LSB, same as Bytes as Field). Compare as two Uint<128> limbs so values never overflow the 248-bit Uint cap. Callers must sort pubkeys by ascending commitment. Adds verify3 tests for the OpenZeppelin#629 [A,B,A] case. Fixes OpenZeppelin#629
d5fa119 to
6e11b09
Compare
|
Thanks @0xisk — commits are now signed (SSH). Ready for re-review. |
801845d
into
OpenZeppelin:refactor/multisig-signature-verifier
Types of changes
Fixes #629
Summary
EcdsaSignerManager.verifyonly rejected adjacent duplicate commitments:For
n >= 3, a presentation like[A, B, A]passed every check and counted the same signer twice toward the threshold (forged quorum). The module already documented that the adjacent check was only sufficient for at most 2 signers; this PR implements the sorted / strictly-increasing fix noted in #629 and in that notice.Approach
Require presented commitments to be strictly increasing under Compact's
Bytes→integer embedding (first byte is LSB — the same order asBytes as Field):prevCommitmentfor the first iteration.EcdsaSignerManager: duplicate or unsorted signer.Why not
as Field as Uint<248>? Full 32-byte hash commitments often exceed Compact's maxUint(2^248 - 1). That cast compiles but panics at runtime. Comparison uses two 16-byte little-endian limbs asUint<128>(always in range):Call-site impact (why "breaking" is checked)
n >= 2, callers must presentpubkeys/signaturessorted by ascending commitment (first-byte-LSB order). Previously any order of two distinct keys worked.verify<2>only; they remain safe if they sort (or already present in ascending order).verifywithn >= 3is fixed.Tests
verify3for a 3-of-N presentation path.[A, B, A]rejects; adjacent duplicate in a triple rejects.SKIP_ZK=true yarn vitest run src/multisig/test/EcdsaSignerManager.test.ts→ 11/11 passed.PR Checklist
Further comments
Base branch:
refactor/multisig-signature-verifier(#628).EcdsaSignerManageris not onmain; targetingmainwould be incorrect.Alternatives considered: bitmap of registered-signer indices (also noted in #629) — stronger without a sort requirement, but needs more fold state / registry coupling. Strict order keeps O(1) fold state and matches the module's documented direction.