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 aa037de..c170638 100644 --- a/src/onchain/secp256k1/Secp256k1.sol +++ b/src/onchain/secp256k1/Secp256k1.sol @@ -142,6 +142,21 @@ library Secp256k1 { return SecretKey.unwrap(sk); } + /// @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) { + if (!sk.isValid()) { + revert("SecretKeyInvalid()"); + } + + return Secp256k1Arithmetic.G().mulToAddress(sk.asUint()); + } + //-------------------------------------------------------------------------- // Public Key diff --git a/src/onchain/secp256k1/Secp256k1Arithmetic.sol b/src/onchain/secp256k1/Secp256k1Arithmetic.sol index a7045be..95a24a6 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 // @@ -319,7 +324,43 @@ library Secp256k1Arithmetic { return result; } - // TODO: Provide verifyMul(point,scalar,want) using ecrecover? + /// @dev Returns the product of point `point` and scalar `scalar` as + /// address. + /// + /// @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) + { + if (scalar >= Q) { + revert("ScalarMustBeFelt()"); + } + + if (scalar == 0 || point.isIdentity()) { + return IDENTITY_ADDRESS; + } + + // 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)); + } //-------------------------------------------------------------------------- // Type Conversions @@ -375,7 +416,7 @@ library Secp256k1Arithmetic { // Return as Point(point.x, point.y). // Note that from this moment, point.z is dirty memory! - // TODO: Zero 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 cd1c5b0..0ec5d3b 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; @@ -235,7 +236,6 @@ contract Secp256k1ArithmeticTest is Test { vm.assume(sk.isValid()); ProjectivePoint memory id = Secp256k1Arithmetic.ProjectiveIdentity(); - assertTrue(wrapper.mul(id, sk.asUint()).isIdentity()); } @@ -249,6 +249,47 @@ contract Secp256k1ArithmeticTest is Test { wrapper.mul(point, scalar); } + // -- 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.mulToAddress(point, scalar); + } + //-------------------------------------------------------------------------- // Type Conversions @@ -587,6 +628,14 @@ contract Secp256k1ArithmeticWrapper { return point.mul(scalar); } + function mulToAddress(Point memory point, uint scalar) + public + pure + returns (address) + { + return point.mulToAddress(scalar); + } + //-------------------------------------------------------------------------- // Type Conversions