Skip to content

Instantly share code, notes, and snippets.

@paulwongx
Last active August 16, 2022 22:19
Show Gist options
  • Save paulwongx/0e99687565b785b027660ca5a525c8c0 to your computer and use it in GitHub Desktop.
Save paulwongx/0e99687565b785b027660ca5a525c8c0 to your computer and use it in GitHub Desktop.
Ethers.js
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
contract ECDSARecover {
using ECDSA for bytes32;
using ECDSA for bytes;
struct Coupon {
bytes32 r;
bytes32 s;
uint8 v;
}
enum CouponType {
Genesis,
Author,
Presale
}
constructor() {}
function recoverSignature(
bytes32 digest,
Coupon memory coupon // to be changed back
) external pure returns (address) {
address signer = ecrecover(digest, coupon.v, coupon.r, coupon.s);
require(signer != address(0), "ECDSA: invalid signature"); // Added check for zero address
return signer;
}
function getCouponType() pure external returns (CouponType) {
return CouponType.Genesis;
// >> 0
}
function keccak256Encode() external view returns (bytes32) {
// https://github.com/ethers-io/ethers.js/issues/468
bytes32 payloadHash = keccak256(abi.encode(msg.sender));
return payloadHash;
}
// This is the message hash to recover given the inputs
function manualEthSignedMessage(bytes32 payloadHash) external pure returns (bytes32) {
// bytes32 messageHash1 = payloadHash.toEthSignedMessageHash();
bytes32 messageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", payloadHash));
return messageHash;
}
function verify(bytes32 digest, Coupon memory coupon)
external
pure
returns (address)
{
// Calling ecrecover directly saves about 660 gwei
address signer = ecrecover(digest, coupon.v, coupon.r, coupon.s);
require(signer != address(0), "ECDSA: invalid signature");
return signer;
}
function recover(bytes32 hash, bytes memory signature) public pure returns (address) {
return hash.recover(signature);
}
function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) {
return hash.toEthSignedMessageHash();
}
}
// import { expect } from "chai";
import { expect } from "chai";
import { Contract } from "ethers";
import { ethers } from "hardhat";
// hh test ./test/custom/ECDSARecover.test
// Source: https://github.com/ethers-io/ethers.js/issues/468
// Source:
describe("ECDSARecover", function () {
let ecdsa: Contract;
beforeEach(async function () {
const [owner, other, another, external, adminSigner] =
await ethers.getSigners();
this.owner = owner;
this.other = other;
this.another = another;
this.external = external;
this.adminSigner = adminSigner;
const ECDSA = await ethers.getContractFactory("ECDSARecover");
ecdsa = await ECDSA.deploy();
await ecdsa.deployed();
});
context("tokenURI", async function () {
it("Should recover ECDSA with ethers alone", async function () {
// >> this.adminSigner.address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
// >> this.other.address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
const message = ethers.utils.id(this.other.address);
// console.log("message", message);
// >> message 0x417ebaa8de17c9a73acf7cf3f6c998ca2dd1e993276cc9dd37dfb7fb0cb197a4
const arrayifiedMessage = ethers.utils.arrayify(message);
// console.log("arrayifiedMessage", arrayifiedMessage);
// arrayifiedMessage Uint8Array(32) [
// 65, 126, 186, 168, 222, 23, 201,
// 167, 58, 207, 124, 243, 246, 201,
// 152, 202, 45, 209, 233, 147, 39,
// 108, 201, 221, 55, 223, 183, 251,
// 12, 177, 151, 164
// ]
const signature = await this.adminSigner.signMessage(arrayifiedMessage);
// console.log("signature", signature);
// >> signature 0x92b2d891a59de570bb495ef4b6779da3024f066e60d5d7c041c35134932f7954190bbadfab3f1ea8bb5b39715a7c510a9d87e36f554782df73812a85bf50802f1c
// eslint-disable-next-line no-unused-vars
const splitSignature = ethers.utils.splitSignature(signature);
// console.log("splitSignature", splitSignature);
// splitSignature {
// r: '0x92b2d891a59de570bb495ef4b6779da3024f066e60d5d7c041c35134932f7954',
// s: '0x190bbadfab3f1ea8bb5b39715a7c510a9d87e36f554782df73812a85bf50802f',
// _vs: '0x990bbadfab3f1ea8bb5b39715a7c510a9d87e36f554782df73812a85bf50802f',
// recoveryParam: 1,
// v: 28,
// yParityAndS: '0x990bbadfab3f1ea8bb5b39715a7c510a9d87e36f554782df73812a85bf50802f',
// compact: '0x92b2d891a59de570bb495ef4b6779da3024f066e60d5d7c041c35134932f7954990bbadfab3f1ea8bb5b39715a7c510a9d87e36f554782df73812a85bf50802f'
// }
const recoveredSignature = ethers.utils.verifyMessage(
ethers.utils.arrayify(message),
signature
);
// console.log("recoveredSignature", recoveredSignature);
// >> recoveredSignature 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
expect(recoveredSignature).to.be.equal(this.adminSigner.address);
});
it("Should recover ECDSA with ethers template example alone", async function () {
// >> this.adminSigner.address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
// >> this.other.address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
const payload = ethers.utils.defaultAbiCoder.encode(
["address"],
[this.other.address]
);
// console.log("payload", payload);
// >> payload 0x00000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8
const payloadHash = ethers.utils.keccak256(payload);
console.log("payloadHash", payloadHash);
// >> payloadHash 0x7ceb58780fb137bb02223b79c88bc6404f736f8bb4d1f0895d9884122804fb73
const arrayifiedMessage = ethers.utils.arrayify(payloadHash);
// console.log("arrayifiedMessage", arrayifiedMessage);
// arrayifiedMessage Uint8Array(32) [
// 124, 235, 88, 120, 15, 177, 55, 187,
// 2, 34, 59, 121, 200, 139, 198, 64,
// 79, 115, 111, 139, 180, 209, 240, 137,
// 93, 152, 132, 18, 40, 4, 251, 115
// ]
const signature = await this.adminSigner.signMessage(arrayifiedMessage);
// console.log("signature", signature);
// signature 0xb86d705ee7460aa646b4a948423e48b355e5be1564027f871b98197d23c3ba133b0896356c646db53a07f384a6fc3f55239fc254a206272e76e955ecc32c91401b
// verifyMessage adds the Ethereum header to the digest
const recoveredSignature = ethers.utils.verifyMessage(
ethers.utils.arrayify(payloadHash),
signature
);
// console.log("recoveredSignature", recoveredSignature);
// >> recoveredSignature 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
expect(recoveredSignature).to.be.equal(this.adminSigner.address);
});
it("Should recover from the contract alone", async function () {
// >> this.adminSigner.address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
// >> this.other.address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
const payloadHash = await ecdsa.connect(this.other).keccak256Encode();
// console.log("payloadHash", payloadHash);
// >> payload 0x7ceb58780fb137bb02223b79c88bc6404f736f8bb4d1f0895d9884122804fb73
const arrayifiedMessage = ethers.utils.arrayify(payloadHash);
const signature = await this.adminSigner.signMessage(arrayifiedMessage);
// console.log("signature", signature);
// >> signature 0xb86d705ee7460aa646b4a948423e48b355e5be1564027f871b98197d23c3ba133b0896356c646db53a07f384a6fc3f55239fc254a206272e76e955ecc32c91401b
const ethSignedPayloadHash = await ecdsa.manualEthSignedMessage(
payloadHash
);
// console.log("ethSignedPayloadHash", ethSignedPayloadHash);
// >> ethSignedPayloadHash 0x03f1224c7cac830aaed436cbba65287e9cefe9d3b5778c84c61f84cfbb9f6543
const splitSignature = ethers.utils.splitSignature(signature);
const recoveredSignature = await ecdsa.verify(
ethSignedPayloadHash,
splitSignature
);
// console.log("recoveredSignature", recoveredSignature);
// >> 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
expect(recoveredSignature).to.be.equal(this.adminSigner.address);
});
it.only("Should recover using ECDSA methods", async function () {
// >> this.other.address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
const payloadHash = await ecdsa.connect(this.other).keccak256Encode();
// >> payload 0x7ceb58780fb137bb02223b79c88bc6404f736f8bb4d1f0895d9884122804fb73
const arrayifiedMessage = ethers.utils.arrayify(payloadHash);
const signature = await this.adminSigner.signMessage(arrayifiedMessage);
// console.log("signature", signature);
const ethSignedPayloadHash = await ecdsa.toEthSignedMessageHash(
payloadHash
);
// console.log("ethSignedPayloadHash", ethSignedPayloadHash);
// >> ethSignedPayloadHash 0x03f1224c7cac830aaed436cbba65287e9cefe9d3b5778c84c61f84cfbb9f6543
const recoveredSignature = await ecdsa.recover(
ethSignedPayloadHash,
signature
);
// console.log("recoveredSignature", recoveredSignature);
// >> recoveredSignature 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
expect(recoveredSignature).to.be.equal(this.adminSigner.address);
});
it("Recovering the signatures", async function () {
console.log("this.other.address", this.other.address);
// >> this.other.address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
console.log("this.adminSigner.address", this.adminSigner.address);
// >> this.adminSigner.address 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
});
});
});
const { ethers } = require("ethers");
const privateKey =
"0x0123456789012345678901234567890123456789012345678901234567890123";
const wallet = new ethers.Wallet(privateKey);
// console.log("signer address", wallet.address);
// >> 0x14791697260E4c9A71f18484C9f997B308e59325
// export function keccak256(types: ReadonlyArray<string>, values: ReadonlyArray<any>) {
// return hashKeccak256(pack(types, values));
// }
const solidityKeccak256 = async (message) => {
const messageHash = ethers.utils.solidityKeccak256(["string"], [message]);
// console.log("messageHash: ", messageHash);
return messageHash;
};
// export function keccak256(data: BytesLike): string {
// return '0x' + sha3.keccak_256(arrayify(data));
// }
const keccak256 = async (message) => {
const messageHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes(message));
// console.log("messageHash: ", messageHash);
return messageHash;
};
// export function id(text: string): string {
// return keccak256(toUtf8Bytes(text));
// }
const id = async (message) => {
const messageHash = ethers.utils.id(message);
// console.log("messageHash: ", messageHash);
return messageHash;
};
const signAndCompare = async (messageHash) => {
const signature = await wallet.signMessage(
ethers.utils.arrayify(messageHash)
);
// console.log("signature:", signature);
// >> 0xe0400ffd1dce1f2076c812fcbdb622c3d5a49916540292d43fae7e400aba64e52695742c4810a3f27eb55deec5adfc287eae9e9442d257867be1f1210c895f311b
const arrayified = ethers.utils.arrayify(messageHash);
// console.log("arrayified", arrayified);
// >> Uint8Array(32) [
// 71, 23, 50, 133, 168, 215, 52, 30,
// 94, 151, 47, 198, 119, 40, 99, 132,
// 248, 2, 248, 239, 66, 165, 236, 95,
// 3, 187, 250, 37, 76, 176, 31, 173
// ]
const recoverAddress = ethers.utils.verifyMessage(arrayified, signature);
return recoverAddress === wallet.address;
};
const toUtf8Bytes = async (message) => {
const utf8Bytes = ethers.utils.toUtf8Bytes(message);
// console.log("utf8Bytes", utf8Bytes);
return utf8Bytes;
};
const hexlify = async (utf8Bytes) => {
const hexString = ethers.utils.hexlify(utf8Bytes);
// console.log("hexString", hexString);
return hexString;
};
const arrayify = async (hexString) => {
const uint8Array = ethers.utils.arrayify(hexString);
console.log("uint8Array", uint8Array);
return uint8Array;
};
/**
* Keccack always returns a 32 byte array
* The message hash has 64 characters because you need two characters to represent 1 byte
* Each character is 8 bits or 1 byte
*/
solidityKeccak256("ABC")
.then(signAndCompare)
.then((authentic) => console.log(`authentic ${authentic}`));
// >> messageHash: 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
// >> true
keccak256("ABC")
.then(signAndCompare)
.then((authentic) => console.log(`authentic ${authentic}`));
// >> messageHash: 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
// >> true
id("ABC")
.then(signAndCompare)
.then((authentic) => console.log(`authentic ${authentic}`));
// >> messageHash: 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
// >> true
/**
* UTF-8: Unicode Transformation Format-8
* These functions show the decimal equivalent of characters
* utf8 Table: https://www.charset.org/utf-8
*/
toUtf8Bytes(123); // <- Must be text
// >> utf8Bytes Uint8Array(0) []
toUtf8Bytes("0123");
// >> Uint8Array(4) [ 48, 49, 50, 51 ]
toUtf8Bytes("abc");
// >> Uint8Array(3) [ 97, 98, 99 ]
toUtf8Bytes("ABC");
// >> Uint8Array(3) [ 65, 66, 67 ]
/**
* These functions show hex string in the format of 0x + UTF-8 Hex characters
* For example, A is 65 in utf8 and 41 in UTF8-Hex
* utf8-Hex can be in lowercase, it's all the same
* utf8 Table: https://www.charset.org/utf-8
*/
toUtf8Bytes(123).then(hexlify);
// >> 0x
toUtf8Bytes("0123").then(hexlify);
// >> 0x30313233
toUtf8Bytes("abc").then(hexlify);
// >> 0x616263
toUtf8Bytes("ABC").then(hexlify);
// >> 0x414243
toUtf8Bytes("JKL").then(hexlify);
// >> 0x4a4b4c
toUtf8Bytes("ABC").then(hexlify).then(arrayify);
// >> Uint8Array(3) [ 65, 66, 67 ]
toUtf8Bytes("ABC").then(arrayify);
// >> Uint8Array(3) [ 65, 66, 67 ]
/* eslint-disable node/no-unpublished-require */
const { ethers } = require("ethers");
const Web3 = require("web3");
const compare = (data1, data2) => {
return data1 === data2;
};
const hexToBytes = async (sig) => {
const wBytes = Web3.utils.hexToBytes(sig);
// console.log("wBytes", wBytes);
const eBytes = ethers.utils.arrayify(sig);
// console.log("eBytes", eBytes);
return (
wBytes.length === eBytes.length && wBytes.every((v, i) => v === eBytes[i])
);
};
const bytesToHex = async (bytes) => {
const wHex = Web3.utils.bytesToHex(bytes);
// console.log("wHex", wHex);
const eHex = ethers.utils.hexlify(bytes);
// console.log("eHex", eHex);
return compare(wHex, eHex);
};
const sha3 = async (message) => {
const wsha3 = Web3.utils.sha3(message);
// console.log("wsha3", wsha3);
const esha3 = ethers.utils.id(message);
// console.log("esha3", esha3);
return compare(wsha3, esha3);
};
const sign = async (message, privateKey) => {
const web3 = new Web3();
const wHash = Web3.utils.sha3(message);
const wSignature = await web3.eth.accounts.sign(wHash, privateKey);
// console.log("wSignature", wSignature);
// wSignature {
// message: '0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8',
// messageHash: '0x6f5fabc4608e7289cbb18e2dfd8fa000a2efacbf112e061ccfa4778e4ea79ef6',
// v: '0x1b',
// r: '0x4c82baef8342bf38d0834816f3a62107f925aa1ca0aea176bd10075dba0f660b',
// s: '0x75c7ec29efc8b2e6421662c3105c194f20a10ff64ec5e5a628e482405c861c34',
// signature: '0x4c82baef8342bf38d0834816f3a62107f925aa1ca0aea176bd10075dba0f660b75c7ec29efc8b2e6421662c3105c194f20a10ff64ec5e5a628e482405c861c341b'
// }
// console.log("wSignature.signature", wSignature.signature);
const signer = new ethers.Wallet(privateKey);
const eHash = ethers.utils.id(message);
const eSignature = await signer.signMessage(ethers.utils.arrayify(eHash));
// console.log("eSignature", eSignature);
const splitESignature = ethers.utils.splitSignature(eSignature);
// console.log("splitESignature", splitESignature);
// splitESignature {
// r: '0x4c82baef8342bf38d0834816f3a62107f925aa1ca0aea176bd10075dba0f660b',
// s: '0x75c7ec29efc8b2e6421662c3105c194f20a10ff64ec5e5a628e482405c861c34',
// _vs: '0x75c7ec29efc8b2e6421662c3105c194f20a10ff64ec5e5a628e482405c861c34',
// recoveryParam: 0,
// v: 27,
// yParityAndS: '0x75c7ec29efc8b2e6421662c3105c194f20a10ff64ec5e5a628e482405c861c34',
// compact: '0x4c82baef8342bf38d0834816f3a62107f925aa1ca0aea176bd10075dba0f660b75c7ec29efc8b2e6421662c3105c194f20a10ff64ec5e5a628e482405c861c34'
// }
return compare(wSignature.signature, eSignature);
};
sha3("ABC");
// wsha3 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
// esha3 0xe1629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8
bytesToHex(ethers.utils.toUtf8Bytes("ABC"));
// wHex 0x414243
// eHex 0x414243
hexToBytes("0x414243");
// wBytes [ 65, 66, 67 ]
// eBytes Uint8Array(3) [ 65, 66, 67 ]
const privateKey =
"0x0123456789012345678901234567890123456789012345678901234567890123";
sign("ABC", privateKey).then(console.log);
// const sig = web3.eth.accounts.sign("ABC", privateKey);
const signer = new ethers.Wallet(privateKey);
// console.log(
// "signer._mnemonic",
// signer._signingKey((x) => x)
// );
// SigningKey {
// curve: 'secp256k1',
// privateKey: '0x0123456789012345678901234567890123456789012345678901234567890123',
// publicKey: '0x046655feed4d214c261e0a6b554395596f1f1476a77d999560e5a8df9b8a1a3515217e88dd05e938efdd71b2cce322bf01da96cd42087b236e8f5043157a9c068e',
// compressedPublicKey: '0x026655feed4d214c261e0a6b554395596f1f1476a77d999560e5a8df9b8a1a3515',
// _isSigningKey: true
// }
/** Initiating a contract for testing */
// ethers
function ethersTestContract() {
describe("ECDSA", function () {
let other; // typeof SignerWithAddress
let ecdsa; // typeof Contract
beforeEach(async function () {
const ECDSAMock = await ethers.getContractFactory("ECDSAMock");
ecdsa = await ECDSAMock.deploy();
await ecdsa.deployed();
this.ecdsa = ecdsa;
[other] = await ethers.getSigners();
});
});
}
// web3
function web3TestContract() {
describe("ECDSA", function (accounts) {
const [other] = accounts;
beforeEach(async function () {
const ECDSAMock = artifacts.require("ECDSAMock");
this.ecdsa = await ECDSAMock.new();
});
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment