This software is unaudited and should not be used in production. Use at your own risk.
DO NOT use versions of this library < v0.30.0
This library contained a critical vulnerability found by @olehmisar. The circuits were critically underconstrained, allowing anyone to impersonate public Ethereum addresses.
See more details here.
ECRecover Noir includes tools to help prove secp256k1 signatures (Ethereum's curve) in Noir Circuits.
The goal of this lib is to have a function, which mimics the functionality of Solidity's ecrecover
function, where given a hash
and a signature
, it returns the Ethereum address that signed the message. Unfortunately, for this naive implmentation we need the Ethereum account's full Public key (two concatenated 32byte x and y coordinates on the secp256k1 curve). An address's Public key is safe for public storage, and can actually be determined from any signed message, or an Etheruem transaction. See this section for a typescript helper. There are also inputs in the Prover.toml file for reference.
A fully fledged ecrecover
may be released in the future, but it would be an incredibly massive, unoptimized circuit. For now, this is a good starting point.
In your Nargo.toml
file, add the following dependency:
[dependencies]
ecrecover = { tag = "v0.30.0", git = "https://github.com/colinnielsen/ecrecover-noir" }
use dep::std;
use dep::ecrecover;
fn main(
pub_key_x: [u8; 32],
pub_key_y: [u8; 32],
signature: [u8; 64],
hashed_message: pub [u8; 32]
) -> pub Field {
let address = ecrecover::ecrecover(pub_key_x, pub_key_y, signature, hashed_message);
std::println(address);
address // address is represented as `Field`, but is constrained to be within 160 bits
}
Behind the scenes, this library uses the PubKey
struct to handle different operations in a module called secp256k1
.
struct PubKey {
pub_x: [u8; 32],
pub_y: [u8; 32],
}
Initialize a PubKey
struct using a the x and y coordinates of the public key:
NOTE: given Nargo v0.6.0
, this is the most efficient initializer, and should be preferred. The other initializers will match efficiency once the unconstrained
keyword is implemented.
use dep::ecrecover;
// ...
let key = ecrecover::secp256k1::PubKey::from_xy(pub_x, pub_y);
A helper constructor, which splits the inputs a concatenated public key for you:
use dep::ecrecover;
// ...
let key = ecrecover::secp256k1::PubKey::from_unified(pub_key);
Another helper constructor, which assumes the 0x04 prefix for uncompressed public keys (pub_key[0] == 0x04
is checked):
use dep::ecrecover;
// ...
let key = ecrecover::secp256k1::PubKey::from_uncompressed(pub_key);
The following methods are available on the PubKey
struct:
Returns whether the signature is valid for the hashed message:
use dep::ecrecover;
// ...
let key = ecrecover::secp256k1::PubKey::from_xy(pub_x, pub_y);
let is_valid = key.verify_sig(signature, hashed_message);
std::println(is_valid); // true
Converts the PubKey
to an Ethereum address as a 160 bit Field element - this is done by keccak256 hashing the concatenated x and y coordinates of the public key, and returning the last 160 bits:
let key = ecrecover::secp256k1::PubKey::from_xy(pub_x, pub_y);
let addr = key.to_eth_address();
std::println(addr); // 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
A convenience method, which combines the verify_sig
and to_eth_address
methods into one. This is the method used in the main ecrecover
function:
let key = ecrecover::secp256k1::PubKey::from_xy(pub_x, pub_y);
let addr = key.ecrecover(signature, hashed_message);
std::println(addr); // 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
You can use the following typescript + ethers code to get an account's public key from a signed message:
import { ethers } from "ethers";
async function main() {
// hardhat wallet 0
const sender = new ethers.Wallet(
"ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
);
console.log("\x1b[34m%s\x1b[0m", "signing message π: ", message);
const signature = await sender.signMessage(message); // get the signature of the message, this will be 130 bytes (concatenated r, s, and v)
console.log("signature π: ", signature);
let pubKey_uncompressed = ethers.utils.recoverPublicKey(digest, signature);
console.log("uncompressed pubkey: ", pubKey_uncompressed);
// recoverPublicKey returns `0x{hex"4"}{pubKeyXCoord}{pubKeyYCoord}` - so slice 0x04 to expose just the concatenated x and y
// see https://github.com/indutny/elliptic/issues/86 for a non-explanation explanation π
let pubKey = pubKey_uncompressed.slice(4);
let pub_key_x = pubKey.substring(0, 64);
console.log("public key x coordinate π: ", pub_key_x);
let pub_key_y = pubKey.substring(64);
console.log("public key y coordinate π: ", pub_key_y);
}
main();
This project is licensed under the MIT License. See the LICENSE file for details.