I'm trying to do some simple signature verification using signer.signMessage with web3Provider but I think I'm missing something. I can't quite get the addresses to match up. Here's my code:
PERSON_1 private key: da4a4ffa13bcce17a4fd6f98484ad450a8b17e0bf3f953581d0566ae23c5ac6d
const PERSON_1 = accounts[2]
const signThing = "0x50e255a73d200fd6365e4c58f756df5dd7e26ed02bed3b5a9baca066394fba26";
const signature = await signers[PERSON_1].signMessage(signThing);
const solidityAddress = await exchangeWrappers[PERSON_1].signatureTest(signThing, signature)
console.log("Address from solidity:" + solidityAddress);
console.log("PERSON_1 public: " + PERSON_1);
exchangeWrappers is just an object of ether.js Contracts where the key is the public key. I'm in a truffle environment for reference. This outputs
Address from solidity:0xc43111BcB37D98a9581053cBD12b471c0ae7ceB5
PERSON_1 public: 0xf0c73d32c31cced98d86402dbac4f8e2f125b3a7
and signatureTest is simply:
function signatureTest(
bytes32 message,
bytes sig
)
public
pure
returns (address)
{
return ECRecovery.recover(
ECRecovery.toEthSignedMessageHash(message),
sig
);
}
where the ECRecovery library is from zeppelin:
pragma solidity ^0.4.24;
/**
* @title Eliptic curve signature operations
* @dev Based on https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d
* TODO Remove this library once solidity supports passing a signature to ecrecover.
* See https://github.com/ethereum/solidity/issues/864
*/
library ECRecovery {
/**
* @dev Recover signer address from a message by using their signature
* @param hash bytes32 message, the hash is the signed message. What is recovered is the signer address.
* @param sig bytes signature, the signature is generated using web3.eth.sign()
*/
function recover(bytes32 hash, bytes sig)
internal
pure
returns (address)
{
bytes32 r;
bytes32 s;
uint8 v;
// Check the signature length
if (sig.length != 65) {
return (address(0));
}
// Divide the signature in r, s and v variables
// ecrecover takes the signature parameters, and the only way to get them
// currently is to use assembly.
// solium-disable-next-line security/no-inline-assembly
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
// Version of signature should be 27 or 28, but 0 and 1 are also possible versions
if (v < 27) {
v += 27;
}
// If the version is correct return the signer address
if (v != 27 && v != 28) {
return (address(0));
} else {
// solium-disable-next-line arg-overflow
return ecrecover(hash, v, r, s);
}
}
/**
* toEthSignedMessageHash
* @dev prefix a bytes32 value with "\x19Ethereum Signed Message:"
* and hash the result
*/
function toEthSignedMessageHash(bytes32 hash)
internal
pure
returns (bytes32)
{
// 32 is the length in bytes of hash,
// enforced by the type signature above
return keccak256(
abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)
);
}
}
Any ideas? I'm baffled why the addresses don't match :confused: .
Thanks!
I will look more into this when I get home, but a quick note, I believe, TestRPC (which is what is used by truffle) has a bug where 50% of all signed messages have the wrong signature... Can you try signing 3 or 4 different messages and see if some work?
(There will be an ethers replacement for TestRPC soon ;))
Oh, but one quick note I just saw above is that you are signing a string, which means your length is 66 bytes long. If you want to sign the 32 byte binary value of the string as a bytes32, you must pass in the bytes; you can use ethers.utils.arrayify to convert a hex string into a Uint8Array.
Wow I can't believe I missed that. Thank you! I'll close this now. Would you be open to a documentation PR on v4 to add a simple signing example like this one for future users? I can also add some snippets that I've pieced together from reading various issues in the past week and put together a truffle example.
@ricmoo I also just want to say thank you for all the support you give for this library and for the library itself. It is really a step up from web3, your responses to issues are incredibly helpful and inhumanly fast, and the docs are great.
The v4 documentation is still entirely local, so don't make any PR for it yet, otherwise it is just something else to merge. I think there is a task already open to add an example to the cookbook, but I'll add one now so I don't forget, then it will just exist once the v4 docs are up. :)
I'd also like to get some "from start to finish" examples for tutorials on how to build a dapp. I don't use truffle myself, so it'd be good to get someone with knowledge of it to give a full example using truffle.
I try to respond from my phone when I can, which is why sometimes the code snippets are a bit sparse (why is the triple-backtick so hard to type on iOS?), but I try to answer what I can as soon as I can. I do appreciate the appreciation though. Thanks! :)
@mrwillis Mind sharing an updated working snippet? I've been running into the exact same problem and tried to arrayify, hexlify, toUtf8Bytes and other combinations but I always end up in the same mess.
Thanks!
Are you using v4 or v3?
v3
I have the same signatureTest function as defined above by @mrwillis using OpenZeppelin's ECRecovery:
> w = ethers.Wallet.createRandom()
> msg = ethers.utils.id('hello world')
'0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad'
> sig = w.signMessage(msg)
'0xd30d4b49e51df0428a9aa8da22d45f872298ab58f9fe0d5c8799de67026c03fc279e950d2f983fcd0f39b196f5126f6df93c0ebe3d1f4d2d0b3c26a09ec79d911c'
> contract.signatureTest(msg, sig)
'0xa6a6d419ef2ff9f5f6fdb6fcae1aa8233cd040df'
> w.address
'0x026fefe9B869485Aa1fcF39c851a082BC2E29315'
Err, never mind. I thought I tried this before but it actually works (using w.signMessage(ethers.utils.arrayify(msg)) above)
Awesome! :)
I need to add a section to the documentation to explain why we need to do this, but basically strings are treated as strings and Uint8Arrays are treated like binary data.
The problem, I think, is that almost anywhere else in the library you can use a hex string as data, but in this one case both strings and binary data make sense. Any feedback or suggestions to better explain this in the docs are appreciated. I will likely load it up with lots of examples. :)
Most helpful comment
Oh, but one quick note I just saw above is that you are signing a string, which means your length is 66 bytes long. If you want to sign the 32 byte binary value of the string as a bytes32, you must pass in the bytes; you can use
ethers.utils.arrayifyto convert a hex string into a Uint8Array.