From 3bddfcbdeb58efaf42b4178e667220f49ea369c3 Mon Sep 17 00:00:00 2001 From: StackOverflowExcept1on <109800286+StackOverflowExcept1on@users.noreply.github.com> Date: Wed, 15 May 2024 13:13:41 +0300 Subject: [PATCH 1/2] feat: add fast point-to-scalar multiplication via ecrecover --- examples/secp256k1/Secp256k1.sol | 6 +- src/onchain/secp256k1/Secp256k1.sol | 14 +++++ src/onchain/secp256k1/Secp256k1Arithmetic.sol | 25 ++++++++ .../secp256k1/Secp256k1Arithmetic.t.sol | 57 ++++++++++++++++--- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/examples/secp256k1/Secp256k1.sol b/examples/secp256k1/Secp256k1.sol index 24b452d..1763cc7 100644 --- a/examples/secp256k1/Secp256k1.sol +++ b/examples/secp256k1/Secp256k1.sol @@ -59,13 +59,15 @@ contract Secp256k1Example is Script { */ // Derive common constructs. - address addr = pk.toAddress(); + address addr1 = sk.toAddress(); + address addr2 = pk.toAddress(); + assert(addr1 == addr2); /* bytes32 digest = pk.toHash(); uint yParity = pk.yParity(); */ console.log("Derived address:"); - console.log(addr); + console.log(addr1); console.log(""); // Serialization. diff --git a/src/onchain/secp256k1/Secp256k1.sol b/src/onchain/secp256k1/Secp256k1.sol index 2a73b51..d30ccbc 100644 --- a/src/onchain/secp256k1/Secp256k1.sol +++ b/src/onchain/secp256k1/Secp256k1.sol @@ -142,6 +142,20 @@ library Secp256k1 { return SecretKey.unwrap(sk); } + /// @dev Returns the address of secret key `sk`. + /// + /// @dev Reverts if: + /// Secret key invalid + function toAddress(SecretKey sk) internal pure returns (address) { + if (!sk.isValid()) { + revert("SecretKeyInvalid()"); + } + + // Use fastMul to compute pk = [sk]G. + Point memory g = Secp256k1Arithmetic.G(); + return g.fastMul(sk.asUint()); + } + //-------------------------------------------------------------------------- // Public Key diff --git a/src/onchain/secp256k1/Secp256k1Arithmetic.sol b/src/onchain/secp256k1/Secp256k1Arithmetic.sol index b5ca7a7..fc8b453 100644 --- a/src/onchain/secp256k1/Secp256k1Arithmetic.sol +++ b/src/onchain/secp256k1/Secp256k1Arithmetic.sol @@ -308,6 +308,31 @@ library Secp256k1Arithmetic { return result; } + /// @dev Returns the product of point `point` and scalar `scalar` + /// as an Ethereum address. + /// + /// See [SEC-1 v2] section 4.1.6 "Public Key Recovery Operation". + function fastMul(Point memory point, uint scalar) + internal + pure + returns (address) + { + if (scalar >= Q) { + revert("ScalarMustBeFelt()"); + } + + if (scalar == 0 || point.isIdentity()) { + // This is the same as `Secp256k1Arithmetic.Identity().intoPublicKey().toAddress()`. + return 0x2dCC482901728b6df477f4fB2F192733A005d396; + } + + uint8 v = uint8(point.yParity()) + 27; + uint r = point.x; + uint s = mulmod(r, scalar, Q); + + return ecrecover(0, v, bytes32(r), bytes32(s)); + } + // TODO: Provide verifyMul(point,scalar,want) using ecrecover? //-------------------------------------------------------------------------- diff --git a/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol b/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol index cd1c5b0..f277327 100644 --- a/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol @@ -26,6 +26,7 @@ contract Secp256k1ArithmeticTest is Test { using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; + using Secp256k1 for Point; using Secp256k1Arithmetic for Point; using Secp256k1Arithmetic for ProjectivePoint; @@ -199,7 +200,8 @@ contract Secp256k1ArithmeticTest is Test { // -- mul function testVectors_ProjectivePoint_mul() public { - ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); + Point memory gPoint = Secp256k1Arithmetic.G(); + ProjectivePoint memory g = gPoint.toProjectivePoint(); uint[] memory scalars; Point[] memory products; @@ -207,20 +209,28 @@ contract Secp256k1ArithmeticTest is Test { for (uint i; i < scalars.length; i++) { Point memory p = wrapper.mul(g, scalars[i]).intoPoint(); + address addr = wrapper.fastMul(gPoint, scalars[i]); assertTrue(p.eq(products[i])); + assertEq(addr, p.intoPublicKey().toAddress()); } } function testFuzz_ProjectivePoint_mul(SecretKey sk) public { vm.assume(sk.isValid()); - ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); + Point memory gPoint = Secp256k1Arithmetic.G(); + ProjectivePoint memory g = gPoint.toProjectivePoint(); - Point memory got = wrapper.mul(g, sk.asUint()).intoPoint(); - Point memory want = sk.toPublicKey().intoPoint(); + Point memory gotPoint = wrapper.mul(g, sk.asUint()).intoPoint(); + address gotAddr = wrapper.fastMul(gPoint, sk.asUint()); - assertTrue(want.eq(got)); + PublicKey memory pk = sk.toPublicKey(); + Point memory wantPoint = pk.intoPoint(); + address wantAddr = pk.toAddress(); + + assertTrue(wantPoint.eq(gotPoint)); + assertEq(gotAddr, wantAddr); } function testFuzz_ProjectivePoint_mul_ReturnsIdentityIfScalarIsZero( @@ -229,14 +239,29 @@ contract Secp256k1ArithmeticTest is Test { assertTrue(wrapper.mul(point, 0).isIdentity()); } + function testFuzz_ProjectivePoint_fastMul_ReturnsIdentityIfScalarIsZero( + Point memory point + ) public { + assertEq( + wrapper.fastMul(point, 0), + Secp256k1Arithmetic.Identity().intoPublicKey().toAddress() + ); + } + function testFuzz_ProjectivePoint_mul_ReturnsIdentityIfPointIsIdentity( SecretKey sk ) public { vm.assume(sk.isValid()); - ProjectivePoint memory id = Secp256k1Arithmetic.ProjectiveIdentity(); + ProjectivePoint memory identity1 = + Secp256k1Arithmetic.ProjectiveIdentity(); + assertTrue(wrapper.mul(identity1, sk.asUint()).isIdentity()); - assertTrue(wrapper.mul(id, sk.asUint()).isIdentity()); + Point memory identity2 = Secp256k1Arithmetic.Identity(); + assertEq( + wrapper.fastMul(identity2, sk.asUint()), + identity2.intoPublicKey().toAddress() + ); } function testFuzz_ProjectivePoint_mul_RevertsIf_ScalarNotFelt( @@ -249,6 +274,16 @@ contract Secp256k1ArithmeticTest is Test { wrapper.mul(point, scalar); } + function testFuzz_ProjectivePoint_fastMul_RevertsIf_ScalarNotFelt( + Point memory point, + uint scalar + ) public { + vm.assume(scalar >= Secp256k1Arithmetic.Q); + + vm.expectRevert("ScalarMustBeFelt()"); + wrapper.fastMul(point, scalar); + } + //-------------------------------------------------------------------------- // Type Conversions @@ -587,6 +622,14 @@ contract Secp256k1ArithmeticWrapper { return point.mul(scalar); } + function fastMul(Point memory point, uint scalar) + public + pure + returns (address) + { + return point.fastMul(scalar); + } + //-------------------------------------------------------------------------- // Type Conversions From 656e7d4fac3b85fa31f561277b73f45752867f07 Mon Sep 17 00:00:00 2001 From: pascal Date: Wed, 26 Jun 2024 14:00:57 +0200 Subject: [PATCH 2/2] refactoring --- src/onchain/secp256k1/Secp256k1.sol | 7 +- src/onchain/secp256k1/Secp256k1Arithmetic.sol | 35 ++++++--- test/onchain/secp256k1/Secp256k1.t.sol | 31 +++++++- .../secp256k1/Secp256k1Arithmetic.t.sol | 78 ++++++++++--------- 4 files changed, 102 insertions(+), 49 deletions(-) diff --git a/src/onchain/secp256k1/Secp256k1.sol b/src/onchain/secp256k1/Secp256k1.sol index d30ccbc..2be959d 100644 --- a/src/onchain/secp256k1/Secp256k1.sol +++ b/src/onchain/secp256k1/Secp256k1.sol @@ -144,6 +144,9 @@ library Secp256k1 { /// @dev Returns the address of secret key `sk`. /// + /// @dev Note that this function is substantially cheaper than computing + /// `sk`'s public key and deriving it's address manually. + /// /// @dev Reverts if: /// Secret key invalid function toAddress(SecretKey sk) internal pure returns (address) { @@ -151,9 +154,7 @@ library Secp256k1 { revert("SecretKeyInvalid()"); } - // Use fastMul to compute pk = [sk]G. - Point memory g = Secp256k1Arithmetic.G(); - return g.fastMul(sk.asUint()); + return Secp256k1Arithmetic.G().mulToAddress(sk.asUint()); } //-------------------------------------------------------------------------- diff --git a/src/onchain/secp256k1/Secp256k1Arithmetic.sol b/src/onchain/secp256k1/Secp256k1Arithmetic.sol index fc8b453..6112334 100644 --- a/src/onchain/secp256k1/Secp256k1Arithmetic.sol +++ b/src/onchain/secp256k1/Secp256k1Arithmetic.sol @@ -48,6 +48,7 @@ struct ProjectivePoint { * - [Yellow Paper]: https://github.com/ethereum/yellowpaper * - [Renes-Costello-Batina 2015]: https://eprint.iacr.org/2015/1060.pdf * - [Dubois 2023]: https://eprint.iacr.org/2023/939.pdf + * - [Vitalik 2018]: https://ethresear.ch/t/you-can-kinda-abuse-ecrecover-to-do-ecmul-in-secp256k1-today/2384 * * @author verklegarden * @custom:repository github.com/verklegarden/crysol @@ -72,6 +73,10 @@ library Secp256k1Arithmetic { /// computed via x^{SQUARE_ROOT_EXPONENT} (mod p). uint private constant SQUARE_ROOT_EXPONENT = (P + 1) / 4; + /// @dev Used as substitute for `Identity().intoPublicKey().toAddress()`. + address private constant IDENTITY_ADDRESS = + 0x2dCC482901728b6df477f4fB2F192733A005d396; + //-------------------------------------------------------------------------- // Secp256k1 Constants // @@ -308,11 +313,13 @@ library Secp256k1Arithmetic { return result; } - /// @dev Returns the product of point `point` and scalar `scalar` - /// as an Ethereum address. + /// @dev Returns the product of point `point` and scalar `scalar` as + /// address. /// - /// See [SEC-1 v2] section 4.1.6 "Public Key Recovery Operation". - function fastMul(Point memory point, uint scalar) + /// @dev Note that this function is substantially cheaper than + /// `mul(ProjectivePoint,uint)(ProjectivePoint)` with the caveat that + /// only the point's address is returned instead of the point itself. + function mulToAddress(Point memory point, uint scalar) internal pure returns (address) @@ -322,19 +329,28 @@ library Secp256k1Arithmetic { } if (scalar == 0 || point.isIdentity()) { - // This is the same as `Secp256k1Arithmetic.Identity().intoPublicKey().toAddress()`. - return 0x2dCC482901728b6df477f4fB2F192733A005d396; + return IDENTITY_ADDRESS; } - uint8 v = uint8(point.yParity()) + 27; + // Note that ecrecover can be abused to perform an elliptic curve + // multiplication with the caveat that the point's address is returned + // instead of the point itself. + // + // For further details, see [Vitalik 2018] and [SEC-1 v2] section 4.1.6 + // "Public Key Recovery Operation". + + uint8 v; + // Unchecked because point.yParity() ∊ {0, 1} which cannot overflow by + // adding 27. + unchecked { + v = uint8(point.yParity() + 27); + } uint r = point.x; uint s = mulmod(r, scalar, Q); return ecrecover(0, v, bytes32(r), bytes32(s)); } - // TODO: Provide verifyMul(point,scalar,want) using ecrecover? - //-------------------------------------------------------------------------- // Type Conversions @@ -389,6 +405,7 @@ library Secp256k1Arithmetic { // Return as Point(point.x, point.y). // Note that from this moment, point.z is dirty memory! + // TODO: Clean dirty memory. assembly ("memory-safe") { p := point } diff --git a/test/onchain/secp256k1/Secp256k1.t.sol b/test/onchain/secp256k1/Secp256k1.t.sol index 263026d..583f299 100644 --- a/test/onchain/secp256k1/Secp256k1.t.sol +++ b/test/onchain/secp256k1/Secp256k1.t.sol @@ -100,10 +100,35 @@ contract Secp256k1Test is Test { // -- asUint - function testFuzz_SecertKey_asUint(uint seed) public { + function testFuzz_SecretKey_asUint(uint seed) public { assertEq(seed, wrapper.asUint(SecretKey.wrap(seed))); } + function testFuzz_SecretKey_toAddress(SecretKey sk) public { + vm.assume(sk.isValid()); + + address got = wrapper.toAddress(sk); + address want = vm.addr(sk.asUint()); + + assertEq(got, want); + } + + function test_SecretKey_toAddress_RevertsIf_SecretKeyInvalid_SecretKeyZero() + public + { + vm.expectRevert("SecretKeyInvalid()"); + wrapper.toAddress(SecretKey.wrap(0)); + } + + function testFuzz_SecretKey_toAddress_RevertsIf_SecretKeyInvalid_SecretKeyGreaterOrEqualToQ( + uint seed + ) public { + uint scalar = _bound(seed, Secp256k1.Q, type(uint).max); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.toAddress(SecretKey.wrap(scalar)); + } + //-------------------------------------------------------------------------- // Test: Public Key @@ -318,6 +343,10 @@ contract Secp256k1Wrapper { return sk.asUint(); } + function toAddress(SecretKey sk) public pure returns (address) { + return sk.toAddress(); + } + //-------------------------------------------------------------------------- // Public Key diff --git a/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol b/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol index f277327..0ec5d3b 100644 --- a/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol @@ -200,8 +200,7 @@ contract Secp256k1ArithmeticTest is Test { // -- mul function testVectors_ProjectivePoint_mul() public { - Point memory gPoint = Secp256k1Arithmetic.G(); - ProjectivePoint memory g = gPoint.toProjectivePoint(); + ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); uint[] memory scalars; Point[] memory products; @@ -209,28 +208,20 @@ contract Secp256k1ArithmeticTest is Test { for (uint i; i < scalars.length; i++) { Point memory p = wrapper.mul(g, scalars[i]).intoPoint(); - address addr = wrapper.fastMul(gPoint, scalars[i]); assertTrue(p.eq(products[i])); - assertEq(addr, p.intoPublicKey().toAddress()); } } function testFuzz_ProjectivePoint_mul(SecretKey sk) public { vm.assume(sk.isValid()); - Point memory gPoint = Secp256k1Arithmetic.G(); - ProjectivePoint memory g = gPoint.toProjectivePoint(); - - Point memory gotPoint = wrapper.mul(g, sk.asUint()).intoPoint(); - address gotAddr = wrapper.fastMul(gPoint, sk.asUint()); + ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); - PublicKey memory pk = sk.toPublicKey(); - Point memory wantPoint = pk.intoPoint(); - address wantAddr = pk.toAddress(); + Point memory got = wrapper.mul(g, sk.asUint()).intoPoint(); + Point memory want = sk.toPublicKey().intoPoint(); - assertTrue(wantPoint.eq(gotPoint)); - assertEq(gotAddr, wantAddr); + assertTrue(want.eq(got)); } function testFuzz_ProjectivePoint_mul_ReturnsIdentityIfScalarIsZero( @@ -239,29 +230,13 @@ contract Secp256k1ArithmeticTest is Test { assertTrue(wrapper.mul(point, 0).isIdentity()); } - function testFuzz_ProjectivePoint_fastMul_ReturnsIdentityIfScalarIsZero( - Point memory point - ) public { - assertEq( - wrapper.fastMul(point, 0), - Secp256k1Arithmetic.Identity().intoPublicKey().toAddress() - ); - } - function testFuzz_ProjectivePoint_mul_ReturnsIdentityIfPointIsIdentity( SecretKey sk ) public { vm.assume(sk.isValid()); - ProjectivePoint memory identity1 = - Secp256k1Arithmetic.ProjectiveIdentity(); - assertTrue(wrapper.mul(identity1, sk.asUint()).isIdentity()); - - Point memory identity2 = Secp256k1Arithmetic.Identity(); - assertEq( - wrapper.fastMul(identity2, sk.asUint()), - identity2.intoPublicKey().toAddress() - ); + ProjectivePoint memory id = Secp256k1Arithmetic.ProjectiveIdentity(); + assertTrue(wrapper.mul(id, sk.asUint()).isIdentity()); } function testFuzz_ProjectivePoint_mul_RevertsIf_ScalarNotFelt( @@ -274,14 +249,45 @@ contract Secp256k1ArithmeticTest is Test { wrapper.mul(point, scalar); } - function testFuzz_ProjectivePoint_fastMul_RevertsIf_ScalarNotFelt( + // -- mulToAddress + + function testFuzz_ProjectivePoint_mulToAddress( + SecretKey sk, + uint scalarSeed + ) public { + vm.assume(sk.isValid()); + + Point memory point = sk.toPublicKey().intoPoint(); + uint scalar = _bound(scalarSeed, 1, Secp256k1Arithmetic.Q - 1); + + address got = wrapper.mulToAddress(point, scalar); + // forgefmt: disable-next-item + address want = point.toProjectivePoint() + .mul(scalar) + .intoPoint() + .intoPublicKey() + .toAddress(); + + assertEq(got, want); + } + + function testFuzz_ProjectivePoint_mulToAddress_ReturnsIdentityIfScalarIsZero( + Point memory point + ) public { + assertEq( + wrapper.mulToAddress(point, 0), + Secp256k1Arithmetic.Identity().intoPublicKey().toAddress() + ); + } + + function testFuzz_ProjectivePoint_mulToAddress_RevertsIf_ScalarNotFelt( Point memory point, uint scalar ) public { vm.assume(scalar >= Secp256k1Arithmetic.Q); vm.expectRevert("ScalarMustBeFelt()"); - wrapper.fastMul(point, scalar); + wrapper.mulToAddress(point, scalar); } //-------------------------------------------------------------------------- @@ -622,12 +628,12 @@ contract Secp256k1ArithmeticWrapper { return point.mul(scalar); } - function fastMul(Point memory point, uint scalar) + function mulToAddress(Point memory point, uint scalar) public pure returns (address) { - return point.fastMul(scalar); + return point.mulToAddress(scalar); } //--------------------------------------------------------------------------