Verificación de la firma personal.sign con pyethereum

Estoy tratando de verificar un mensaje firmado de Metamask con pyethereum. Parece que no puedo recuperar la dirección correcta de la firma. La falta de documentación no ayuda.

En el lado del cliente, tengo metamask firmando con 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"
}

Luego envío la firma, el hash del mensaje y la dirección al backend, donde tengo algo como esto:

>>> 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

He intentado algunas formas de jugar con la codificación, pero creo que todavía me falta algo (con suerte, obvio). ¿Algunas ideas?

¿Agregó el prefijo al mensaje antes de firmarlo? Prefijo aquí: web3js.readthedocs.io/en/1.0/web3-eth-accounts.html#sign
@carver web3js maneja eso automáticamente. "Estos datos son anteriores a UTF-8 HEX decodificados y envueltos de la siguiente manera"
Lo siento, me expresé mal. Lo que quise decir fue que, cuando regeneró el hash del mensaje antes de la recuperación, ¿incluyó el prefijo? Esto parece que tal vez no: web3.sha3(message). Puede usar web3js.readthedocs.io/en/1.0/web3-eth-accounts.html#hashmessage que abstrae el prefijo. (Además, web3.py saldrá pronto con herramientas nativas para esto)
Además, message_hash.encode('utf-8')se ve mal porque es una cadena codificada en hexadecimal. Probablemente quieras algo como codecs.decode(message_hash[2:], 'hex'). Puede que le guste eth_utils.decode_hex(message_hash)más, si está abierto a una dependencia de ethereum-utils.
Si publica el mensaje original, podemos verificar el prefijo.
@carver Esta es toda una gran información, gracias. He actualizado mi publicación con tus correcciones. Sin embargo, sigo viendo el problema. Si tienes otras ideas, me encantaría escucharlas.

Respuestas (1)

Todo en Python parece correcto. El hash del mensaje en JavaScript se genera incorrectamente.

Preparando el hash del mensaje en Javascript

El hash del mensaje debe generarse usando el mensaje original, sin codificarlo primero en hexadecimal.

En la práctica, eso significa reemplazar este JavaScript:

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

con este:

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

Eso te dará el hash del mensaje:0x6e099d83ea72d1ef62e39a501fe000c1458ba5a511510a0e9348b0dfeb298803

Cuando use ese hash de mensaje, recuperará el firmante correcto:0x9283099a29556fcf8fff5b2cea2d4f67cb7a7a8b

Una solución aún mejor:hashMessage()

Con web3.js v1, puede llamar:
message_hash = web3.eth.accounts.hashMessage("I am but a stack exchange post").

V1 está en Beta, a partir de octubre de 2017


Otro error común

(de un borrador anterior de la pregunta)

Omitir el prefijo al codificar el mensaje para la recuperación.

Debido a que web3.personal.sign()agrega el prefijo por usted, es fácil olvidar el prefijo al codificar el mensaje para la recuperación. La mejor solución es volver a utilizar web3.js v1 para web3.eth.accounts.hashMessage().


La opción más simple

A partir de Web3.py v4, hay soporte integrado para recuperar el firmante del mensaje , como:

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)

¿Por qué se llama defunct_hash_message? -- Desafortunadamente, el estándar de mensajes no está bien soportado. Tiene implementaciones ligeramente diferentes en varios nodos y clientes de hardware. Hay algunos formatos de mensajes nuevos que se están debatiendo y, con suerte, pronto obtendrán una adopción más amplia y consistente. Por ahora, Web3.py solo admite explícitamente el formato de mensaje de estilo geth, utilizando el defunct_hash_messagemétodo.

2 problemas potenciales con los que me estoy topando. web3.personal.signrequiere que el mensaje esté codificado en hexadecimal( HookedWalletSubprovider - validateMessage - message was not encoded as hex.), razón por la cual estaba agregando 0xy convirtiendo el mensaje a hexadecimal. O bueno, al menos metamask lo requiere. Además, web3.eth.accounts.hashMessageaún no está ampliamente disponible, pero lo espero con ansias. Gracias por toda la información.
Agregando: llegas con precisión a mi problema. Cambiar la llamada a web3.personal.sign("0x" + message.toHex()...y simplemente usar el mensaje simple cuando el hashing funciona muy bien. ¡Gracias de nuevo!