Geth adds prefix to the message before siginig it in web3.eth.sign
(see JSON-PRC spec).
Without this it can be possible to trick user to sign transaction (more here).
So, the correct code to sign message with web3.eth.sign
and recover address with ethereumjs-util.ecrecover
or (Solidity's ecrecover
) should add the prefix explicitly.
const util = require('ethereumjs-util')
const msg = new Buffer('hello');
const sig = web3.eth.sign(web3.eth.accounts[0], '0x' + msg.toString('hex'));
const res = util.fromRpcSig(sig);
const prefix = new Buffer("\x19Ethereum Signed Message:\n");
const prefixedMsg = util.sha3(
Buffer.concat([prefix, new Buffer(String(msg.length)), msg])
);
const pubKey = util.ecrecover(prefixedMsg, res.v, res.r, res.s);
const addrBuf = util.pubToAddress(pubKey);
const addr = util.bufferToHex(addrBuf);
console.log(web3.eth.accounts[0], addr);
There is misleading inconsistency with ethereumjs-testrpc
as it does not prefix message before signing it in web3.eth.sign
.
According to issue #3731:
Geth prepends the string \x19Ethereum Signed Message:\n<length of message>
to all data before signing it (https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign). If you want to verify such a signature from Solidity, you'll have to prepend the same string in solidity before doing the ecrecovery.
Here's a working example I tested out using truffle:
Example.sol
pragma solidity ^0.4.0;
contract Example {
function testRecovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) returns (address) {
bytes memory prefix = "\x19Ethereum Signed Message:\n32";
bytes32 prefixedHash = sha3(prefix, h);
address addr = ecrecover(prefixedHash, v, r, s);
return addr;
}
}
example.js (test)
var Example = artifacts.require('./Example.sol')
var Web3 = require('web3')
var web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'))
contract('Example', (accounts) => {
var address = accounts[0]
it('ecrecover result matches address', async function() {
var instance = await Example.deployed()
var msg = '0x8CbaC5e4d803bE2A3A5cd3DbE7174504c6DD0c1C'
var h = web3.sha3(msg)
var sig = web3.eth.sign(address, h).slice(2)
var r = `0x${sig.slice(0, 64)}`
var s = `0x${sig.slice(64, 128)}`
var v = web3.toDecimal(sig.slice(128, 130)) + 27
var result = await instance.testRecovery.call(h, v, r, s)
assert.equal(result, address)
})
})
Running test:
$ truffle test
Using network 'development'.
Compiling ./contracts/Example.sol...
Contract: Example
✓ ecrecover result matches address (132ms)
1 passing (147ms)
It's probably better to do the prefixing at the application level instead of in solidity contract since it'll be cheaper.
Related
Best Answer
The ChainId is used as a simple replay attack protection. After the London fork the chainId is mandatory and must be used to sign a transaction otherwise the nodes will not accept it.
This EIP-155 describes how the chainId is added to the signature and why.