4

I'm trying to verify a signed message from Metamask with pyethereum. I can not seem to recover the correct address from the signature. Lack of documentation isn't helping matters.

On the client side, I have metamask signing with web3.personal.sign().

var signer = web3.eth.defaultAccount || web3.eth.accounts[0];
var original_message = "I am but a stack exchange post";
var message = "0x" + original_message.toHex();
var message_hash = web3.sha3('\u0019Ethereum Signed Message:\n' + message.length.toString() + message);
var signature;
web3.personal.sign(message, signer, function(err, res) {
    if (err) console.error(err);
    signature = res;
    console.log({
        "signer": signer,
        "message": message,
        "message_hash": message_hash,
        "signature": signature,
    })
});

{
    message: "0x4920616d20627574206120737461636b2065786368616e676520706f7374"
    message_hash: "0x1a0126ceafb4579293016a4cc3ca0ec753c7d497cda8b3e6ece095c832d92590"
    signature: "0x0cf7e2e1cbaf249175b8e004118a182eb378a0b78a7a741e72a0a34e970b59194aa4d9419352d181a4d1827abbad279ad4f5a7b60da5751b82fec4dde6f380a51b"
    signer: "0x9283099a29556fcf8fff5b2cea2d4f67cb7a7a8b"
}

Then I send the signature, hash of the message, and the address to the backend, where I have something like this:

>>> from ethereum.utils import ecrecover_to_pub, sha3
>>> from eth_utils.hexidecimal import encode_hex, decode_hex, add_0x_prefix
>>> signer = "0x9283099a29556fcf8fff5b2cea2d4f67cb7a7a8b"
>>> message_hash = "0x1a0126ceafb4579293016a4cc3ca0ec753c7d497cda8b3e6ece095c832d92590"
>>> signature = "0x0cf7e2e1cbaf249175b8e004118a182eb378a0b78a7a741e72a0a34e970b59194aa4d9419352d181a4d1827abbad279ad4f5a7b60da5751b82fec4dde6f380a51b"
>>> 
>>> r = int(signature[0:66], 16)
>>> s = int(add_0x_prefix(signature[66:130]), 16)
>>> v = int(add_0x_prefix(signature[130:132]), 16)
>>> if v not in (27,28):
...     v += 27
... 
>>> pubkey = ecrecover_to_pub(decode_hex(message_hash), v, r, s)
>>> assert(encode_hex(sha3(pubkey)[-20:]) == signer)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

I've tried a few ways to mess around with encoding, but I think I'm still missing something(hopefully obvious). Any ideas?

  • Did you prepend the message with the prefix before signing it? Prefix here: web3js.readthedocs.io/en/1.0/web3-eth-accounts.html#sign – carver Oct 4 '17 at 7:09
  • @carver web3js automatically handles that. "This data is before UTF-8 HEX decoded and enveloped as follows" – Mike Shultz Oct 4 '17 at 19:32
  • 1
    Sorry, I misspoke. What I meant was, when you regenerated the hash of the message before recovery, did you include the prefix? This looks like maybe not: web3.sha3(message). You can use web3js.readthedocs.io/en/1.0/web3-eth-accounts.html#hashmessage which abstracts away the prefix. (Also, web3.py will be coming out with native tools for this, soon) – carver Oct 4 '17 at 22:14
  • 1
    Also, message_hash.encode('utf-8') looks wrong because it's a hex encoded string. You probably want something like codecs.decode(message_hash[2:], 'hex'). You may like eth_utils.decode_hex(message_hash) better, if you're open to a dependency on ethereum-utils. – carver Oct 4 '17 at 22:29
  • If you post the original message, we can verify the prefix. – carver Oct 4 '17 at 23:09
5

Everything in Python looks correct. The message hash in JavaScript is being generated incorrectly.

Preparing the Message Hash in Javascript

The message hash must be generated using the original message, without hex-encoding it first.

In practice, that means replacing this JavaScript:

var message = "0x" + original_message.toHex();
var message_hash = web3.sha3(
  '\u0019Ethereum Signed Message:\n' +
  message.length.toString() +
  message
);

with this:

var message_hash = web3.sha3(
  '\u0019Ethereum Signed Message:\n' +
  original_message.length.toString() +
  original_message
);

That will give you the message hash: 0x6e099d83ea72d1ef62e39a501fe000c1458ba5a511510a0e9348b0dfeb298803

When you use that message hash, you will recover the correct signer: 0x9283099a29556fcf8fff5b2cea2d4f67cb7a7a8b

An even better solution: hashMessage()

With web3.js v1, you can call:
message_hash = web3.eth.accounts.hashMessage("I am but a stack exchange post").

V1 is in Beta, as of October 2017


Another Common Mistake

(from an earlier draft of the question)

Leaving out the prefix when hashing the message for recovery.

Because web3.personal.sign() adds the prefix for you, it's easy to forget the prefix when hashing the message for recovery. The best solution is again to use web3.js v1 for web3.eth.accounts.hashMessage().


The Simplest Option

As of Web3.py v4, there is built-in support to recover the message signer, like:

from web3.auto import w3

# If you have the original message, you need to hash it first
from eth_account.messages import defunct_hash_message
message_hash = defunct_hash_message(text=original_message)

# If you begin with the message hash, start here:
signer = w3.eth.account.recoverHash(message_hash, signature=signature)

Why is it called defunct_hash_message? -- Unfortunately, the message standard is not well supported. It has slightly different implementations in a number of nodes and hardware clients. There are some new message formats that are under discussion and will hopefully gain broader, more consistent adoption soon. For now, Web3.py only explicitly supports the geth-style message format, using the defunct_hash_message method.

  • 2 potential problems I'm running into. web3.personal.sign requires the message to be hex encoded(HookedWalletSubprovider - validateMessage - message was not encoded as hex.), which is why I was adding 0x and converting the message to hex. Or well, at least metamask requires it. Also, web3.eth.accounts.hashMessage is not yet widely available, but I look forward to it. Thanks for all the info. – Mike Shultz Oct 16 '17 at 20:53
  • 1
    Adding: you do accurately get to my problem. Changing the call to web3.personal.sign("0x" + message.toHex()... and just using the plain message when hashing works great. Thanks again! – Mike Shultz Oct 16 '17 at 20:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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