It is important to note that this trick does not work for secp256k1’s full public key space as ecrecover
fails if r >= N
(N
being the order of the curve).
Note that r
is set to the public key’s x
coordinate which is defined over the prime field and therefore in [1, P)
with P >= N
. However, the work to find a private key with a public key’s x
coordinate greater than N
is roughly 128 bit of work - and therefore of negligible probability.
PoC:
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
uint constant N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
uint constant P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F;
function ecmul(uint x, uint y, uint scalar) pure returns(address) {
return ecrecover(0, y % 2 != 0 ? 28 : 27, bytes32(x), bytes32(mulmod(scalar, x, N)));
}
function isOnCurve(uint x, uint y) pure returns (bool) {
uint left = mulmod(y, y, P);
uint right = addmod(mulmod(x, mulmod(x, x, P), P), 7, P);
return left == right;
}
function toAddress(uint x, uint y) pure returns (address) {
return address(uint160(uint(keccak256(abi.encode(x, y)))));
}
contract EcmulTest is Test {
function test_ecmulVerify_PoC() public pure {
// Note that [1]P = P.
uint scalar = 1;
uint x;
uint y;
// Make a valid secp256k1 point with x >= N.
x = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffeeffffc2e;
y = 0xa0981151316a5be677beb8ab97d4a15eba3e4638bfdc86038afffe1f445f4496;
require(isOnCurve(x, y));
// Note that ecrecover fails.
assertEq(ecmul(x, y, 1), address(0));
// Now lets try some other point (generator).
x = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798;
y = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8;
require(isOnCurve(x, y));
// Note that ecrecover works as expected.
assertEq(ecmul(x, y, scalar), toAddress(x, y));
}
}