35

I know this question has been asked and answered, but while trying to get everything to work, I've come across some problems. This is what I'm trying to do:

1. take a target string 'Schoolbus'
2. use JSON with geth to eth_sign it
3. obtain v,r,s of signature
4. attempt to verify with a solidity contract, need the hash of 'Schoolbus'

So here's what I got. First of all, we can't all use the same private key, so if someone can verify my work and get a gist of my problem, that would be great.

Pretending that my priv key is '0xd1ade25ccd3d550a7eb532ac759cac7be09c2719', to sign 'Schoolbus', I use

curl -X POST --data '{"jsonrpc":"2.0","method":"eth_sign","params":["0xd1ade25ccd3d550a7eb532ac759cac7be09c2719", "Schoolbus"],"id":1}'

Where I get the result

    0x2ac19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601

I'm using (I might be wrong here)

v=2a
r=c19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407
s=466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601

Then I whipped up my contract, which is a variation of an answer in a related thread:

contract Auth {      
    function verify( bytes32 hash, uint8 v, bytes32 r, bytes32 s) constant returns(address retAddr) {
        retAddr= ecrecover(hash, v, r, s);
    }
}

Because I never got the hash of 'Schoolbus', I tried a couple of things in web3.js (the UTF8 one confused me, I was expecting that {encoding:'hex'} version to be the correct one):

console.log('1 '+ web3.sha3(web3.toHex('Schoolbus'))); //05ab39621b81764697fcfb6ae4fcf6b023cd644721c67c13a49fbd769c75671c
console.log('2 '+ web3.sha3(web3.toHex('Schoolbus'),{encoding:'hex'}));//d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c
console.log('3 '+ web3.sha3('Schoolbus'));//d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c
console.log('4 '+  web3.sha3(  unescape(encodeURIComponent('Schoolbus'))  ) ); //to UTF8 //d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c
console.log('5 '+  web3.sha3(  unescape(encodeURIComponent('Schoolbus')), {encoding:'hex'} ) ); //to UTF8 //8f1cbe7efcf383ffeb1aeaf1e826c778a087153344cbeba144fbe967ad3ab11a

I ended up using this, but don't know why:

0xd030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c

Then I called the contract:

var contDep=web3.eth.contract( [abi def] ).at( contractAddress);

console.log(
    contDep.verify('d030d9a04df643f62a1502b017f51c41a659268091abbd20e2de97b935724d7c', 2a,'c19db245478a06032e69cdbd2b54e648b78431d0a47bd1fbab18f79f820ba407', '466e37adbe9e84541cab97ab7d290f4a64a5825c876d22109f3bf813254e8601')
);

Here's my problem. I keep getting this weird address back. It starts with 0x, it's 20 bytes, but it doesn't have [a-f] in it:

0x3433663632613135303262303137663531633431

If I swapped r and s, I get almost the same result back.
I was wondering if someone can verify my experience, or point out what I was doing wrong. I feel like a crazy person here.

Thanks for all your help.

4 Answers 4

27

I was having the same problem earlier, so I am going to give an extensive answer to how this works. I assume you are using geth as a client. There is an open issue where the geth client returns v in the wrong format, so let's keep in mind that if we get a v that is 0 or 1 we should add 27 to it. If you are running node and have connected web3 to your favorite client:

var msg = web3.sha3('Schoolbus')
var signature = web3.eth.sign(web3.eth.accounts[0], msg)

In my case, the signature is:

0x28c412923e03982efdff078f78bb70eaefe32c11751b0c23858191c18dddc4ba72c3667c07672b97c022beb857afb99c49b7084da1608e20392c274adc7dd5851c

The string represents r, s, and v respectively in that order. To feed it to your Auth contract however, you need to convert v to an uint8 and add make sure to have the hex prefix 0x everywhere:

var r = signature.slice(0, 66)
var s = '0x' + signature.slice(66, 130)
var v = '0x' + signature.slice(130, 132)
v = web3.toDecimal(v)
msg = '0x' + msg

Remember that v should be 27 or 28! If it isn't, set v = v + 27. You can now call your verify function like:

var addr = Auth_instance.verify.call(msg, v, r, s)

and you can check that addr has the same value as web3.eth.accounts[0].

6
  • 1
    Thanks for your incredible insight. I had a hard time finding info on the r,s,v ordering in eth_sign. However, after correcting for r,s,v, and submitting as v,r,s using 27 for 00 and 28 for 01 for v, I am still getting addresses that do not match my eth account address. More importantly, the 20-byte hex I get back has no [a-f] characters in them, so they look decimal, except that they are prefixed with '0x'.
    – shiso
    Commented Mar 5, 2016 at 19:17
  • Are you still using that private key that you gave in the example? It does not appear to be correctly generated. A private key should be 64 hex chars (not including prefix), yours is 40. Also eth_sign takes an address as input, not a private key. You can also play around with ecsign and ecrecover functions from here: github.com/ethereumjs/ethereumjs-util/blob/master/docs/index.md. They deal explicitely with public and private keys and addresses. Let me know if you are still having problems
    – MrChico
    Commented Mar 5, 2016 at 20:41
  • Thank you for pointing out the private key issue. My private key (or address in this case) is straight out of the JSON-RPC.md documentation for eth_sign. I just assumed that if I sent my address as the argument, geth will automatically use the private key associated with that address to sign the message. Calling eth_sign requires me to unlock my account, that's why I assumed. I'll look into the doc that you pointed out to investigate more - I do appreciate it. Docs seem to be at many different locations that are not so obvious.
    – shiso
    Commented Mar 6, 2016 at 6:19
  • As you can see from @MrChico 's answer, the first argument to web3.eth.sign(...) is the address of the account whose private key you want to sign with. So, the value that you are sending, which is 40 hex chars (i.e. 20 bytes) is the correct length for an address. From your comment, it seems that you really are sending the address. Just do not call it a "private key". An address is very much a public piece of information. (It is usually generated in some manner from the public key. Commented Apr 10, 2017 at 21:43
  • Is there a web3.eth.verify(0x..) ? Commented Sep 2, 2017 at 0:13
11

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) {
       /* prefix might be needed for geth only
        * https://github.com/ethereum/go-ethereum/issues/3731
        */
        // bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        // h = sha3(prefix, h);
        address addr = ecrecover(h, v, r, s);

        return addr;
    }
}

Here's some examples demonstrating how to obtain the v, r, and s values using slicing and testing that ecrecover returns the address that signed the message:

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.


Here's a helper library with a method that accepts the a hash of the data and the signature and returns the signing address. The smart contract handles obtaining the v, r, and s values instead of doing it at the application level:

ECVerify.sol

pragma solidity ^0.4.4;

/*
 * @credit https://gist.github.com/axic/5b33912c6f61ae6fd96d6c4a47afde6d
  */
library ECVerify {
  function ecrecovery(bytes32 hash, bytes sig) public returns (address) {
    bytes32 r;
    bytes32 s;
    uint8 v;

    if (sig.length != 65) {
      return 0;
    }

    assembly {
      r := mload(add(sig, 32))
      s := mload(add(sig, 64))
      v := and(mload(add(sig, 65)), 255)
    }

    // https://github.com/ethereum/go-ethereum/issues/2053
    if (v < 27) {
      v += 27;
    }

    if (v != 27 && v != 28) {
      return 0;
    }

    /* prefix might be needed for geth only
     * https://github.com/ethereum/go-ethereum/issues/3731
     */
    // bytes memory prefix = "\x19Ethereum Signed Message:\n32";
    // hash = sha3(prefix, hash);

    return ecrecover(hash, v, r, s);
  }

  function ecverify(bytes32 hash, bytes sig, address signer) public returns (bool) {
    return signer == ecrecovery(hash, sig);
  }
}

Here's are some examples on creating signatures with web3 and testing them:

var ECVerify = artifacts.require('./ECVerify.sol')

contract('ECVerify', (accounts) => {
  it('should return signing address from signature', async () => {
    var account = accounts[0]

    try {
      var instance = await ECVerify.deployed()

      var msg = 'some data'

      var hash = web3.sha3(msg)
      var sig = web3.eth.sign(account, hash)

      var signer = await instance.ecrecovery(hash, sig)
      assert.ok(signer)
    } catch(error) {
      console.error(error)
      assert.equal(error, undefined)
    }
  })

  it('should verify signature is from address', async () => {
    var account = accounts[0]

    try {
      var instance = await ECVerify.deployed()
      var msg = 'some data'

      var hash = web3.sha3(msg)
      var sig = web3.eth.sign(account, hash)

      var verified = await instance.ecverify.call(hash, sig, account)
      assert.ok(verified)
    } catch(error) {
      console.error(error)
      assert.equal(error, undefined)
    }
  })
})

Testing that it works:

$ truffle test

Compiling ./contracts/ECVerify.sol...
Compiling ./contracts/Migrations.sol...


  Contract: ECVerify
    ✓ should return signing address from signature (182ms)
    ✓ should verify signature is from address (142ms)


  2 passing (342ms)

Related

3

Miguel Mota's answer covers the salient details. However the following line ->var sig = web3.eth.sign(account, hash) threw an error, Error: Provided address is invalid, the capitalization checksum test failed, or its an indrect IBAN address which can't be converted. Apparently in versions of web3.js these parameters seem to be reversed and you need to provide msg as first parameter and account/address as second. Here is an example test that worked for me.

var address = accounts[0];
it('ecrecover result matches address', async function() {
    var instance = await Adoption.deployed()
    let msg = 'I really did make this message';
    let prefix = "\x19Ethereum Signed Message:\n" + msg.length
    let h = web3.utils.sha3(prefix+msg)
    console.log(`sha3 hash ${h}`);

    let sig1 = await web3.eth.sign(msg, address);
    console.log(`signature: ${sig1}`)
    var sig = sig1.slice(2)
    var r = `0x${sig.slice(0, 64)}`
    var s = `0x${sig.slice(64, 128)}`
    var v = web3.utils.toDecimal(sig.slice(128, 130)) + 27

    var result = await instance.recoverAddr.call(h, v, r, s)
    console.log(`address: ${address}, result ${result}`)
    assert.equal(result, address)
  })

My corresponding contract code was:

function recoverAddr(bytes32 msgHash, uint8 v, bytes32 r, bytes32 s) public view returns (address) {
        return ecrecover(msgHash, v, r, s);
    }
0
2

For Geth users, the following works. Tested it recently. Firstly, the test contract code is from the top level comment. You can paste that code at https://remix.ethereum.org

pragma solidity ^0.4.0;

contract Example {
    function testRecovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) returns (address) {
       /* prefix might be needed for geth only
        * https://github.com/ethereum/go-ethereum/issues/3731
        */
        // bytes memory prefix = "\x19Ethereum Signed Message:\n32";
        // h = sha3(prefix, h);
        address addr = ecrecover(h, v, r, s);

        return addr;
    }
}

On any webpage load the web3.js file. Then in developer tools console just paste these functions. The call the function verificationScheme(msg) with the msg in ascii plaintext.

function tohex(msg){
    var hexmsg = "";
    for(var i=0; i<msg.length; i++){
        hexmsg += msg.charCodeAt(i).toString(16);
    }
    return "0x"+hexmsg;
}


function verificationScheme(str){
    var msghex = tohex(str);
    var sig = web3.eth.sign(web3.eth.accounts[0], msghex);

    var r = sig.slice(0, 66);
    var s = '0x' + sig.slice(66, 130);
    var v = '0x' + sig.slice(130, 132);
    v = web3.toDecimal(v);

    var verificationMessage = "\x19Ethereum Signed Message:\n" + str.length + str;
    var verificationMessageHash = web3.sha3(verificationMessage);

    return [verificationMessageHash, v, r, s];
}

So, my results were these: eth.accounts[0] in the function code points to "0xebbc50c7afc14c693bfc26868c490ce0819cef4f". So, basically that address was used to sign, and that should be final output from the contract. I called verificationScheme("hello").

I got this array back : ["0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750", 27, "0x43653d23758f13a45c498fc96c8d5d07e9fc24123d967b0cb29c48cd48e4c907", "0x365d7cebd54f5f5298b740b44004f9808a2c9791a1c2d5a7137602a0a2742f28"]

Then I called the Example contract's testRecovery function with all the arguments in that array in that order. I got my orginal address "0xebbc50c7afc14c693bfc26868c490ce0819cef4f" back as the output.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.