Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: add fast point-to-scalar multiplication via ecrecover #19

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions examples/secp256k1/Secp256k1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions src/onchain/secp256k1/Secp256k1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 43 additions & 2 deletions src/onchain/secp256k1/Secp256k1Arithmetic.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
//
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
31 changes: 30 additions & 1 deletion test/onchain/secp256k1/Secp256k1.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -318,6 +343,10 @@ contract Secp256k1Wrapper {
return sk.asUint();
}

function toAddress(SecretKey sk) public pure returns (address) {
return sk.toAddress();
}

//--------------------------------------------------------------------------
// Public Key

Expand Down
51 changes: 50 additions & 1 deletion test/onchain/secp256k1/Secp256k1Arithmetic.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -235,7 +236,6 @@ contract Secp256k1ArithmeticTest is Test {
vm.assume(sk.isValid());

ProjectivePoint memory id = Secp256k1Arithmetic.ProjectiveIdentity();

assertTrue(wrapper.mul(id, sk.asUint()).isIdentity());
}

Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading