ethereumjs / ethereumjs-util

Project is in active development and has been moved to the EthereumJS monorepo.
https://github.com/ethereumjs/ethereumjs-monorepo
Mozilla Public License 2.0
608 stars 272 forks source link

How to use ecrecover? #283

Closed kss-espeo closed 3 years ago

kss-espeo commented 3 years ago

Hi! I need to recover a sender's public key from a signature. I used erecover for that, however it gives me incorrect results.

Consider a following transaction: https://etherscan.io/tx/0x26eed054f1ef51ae7a6c94841469c82a20f2deda84affde461b7d3887677bda8

And a TS code sample:

import {bufferToHex, ecrecover, pubToAddress, toBuffer} from "./src";

const txHash = toBuffer('0x26eed054f1ef51ae7a6c94841469c82a20f2deda84affde461b7d3887677bda8');
const r = toBuffer('0x5fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15b');
const s = toBuffer('0x121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c');
const v = 0x26;
const chainId = 0x1;
const pubKey = ecrecover(txHash, v, r, s, chainId);
const addr = pubToAddress(pubKey);
const addrFromApiPubKey = pubToAddress(Buffer.from('54a1c393389571253d4095a973c59ce63a30e540c2931b80d1654248e3a7271ef16f92c5ec534213877fcd5e0be82b242a6b93bb9ada6dd432df76a10c3f71c9', 'hex'));
console.log('pub key:           ' + bufferToHex(pubKey));
console.log('expected pub key:  0x54a1c393389571253d4095a973c59ce63a30e540c2931b80d1654248e3a7271ef16f92c5ec534213877fcd5e0be82b242a6b93bb9ada6dd432df76a10c3f71c9');
console.log('addr:              ' + bufferToHex(addr));
console.log('expected addr:     ' + bufferToHex(addrFromApiPubKey));

It prints a following output:

pub key:           0x622d3ee76c277d3b95397db327f4a20d4c074fb83fc6d17c96e40abcf115967542de2f5c3a4e926e0ef3b2254f340f8e8a8a717acf7f4ed8680a95b013cd900a
expected pub key:  0x54a1c393389571253d4095a973c59ce63a30e540c2931b80d1654248e3a7271ef16f92c5ec534213877fcd5e0be82b242a6b93bb9ada6dd432df76a10c3f71c9
addr:              0xd180aa4e4790fa2f2de549c35c90eb7232b9450e
expected addr:     0x53b3779f4833116fcb87ebdbdccb61141eed7f87

I suppose this means I am using erecover incorrectly. So what am I doing wrong?

cgewecke commented 3 years ago

@kss-espeo

I think you might need to begin with the "raw transaction hash" rather than the transaction id.

The raw tx for 0x26eed054f1ef51ae7a6c94841469c82a20f2deda84affde461b7d3887677bda8 can be found at this etherscan link:

The @ethereumjs/tx library executes the ecrecover logic you're looking for in the method getSenderPublicKey

Usage for your example looks like:

import { Transaction } from '@ethereumjs/tx';

const rawTx = toBuffer('0xf86b0b85250523760082520894eafaf9bb8f35235d0df61275e86fd65d9ef2c3f9870aaa0065c66b8b8026a05fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15ba0121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c');
const tx = Transaction.fromRlpSerializedTx(rawTx);
const pubKey = tx.getSenderPublicKey();

console.log(bufferToHex(pubKey))
> 0x54a1c393389571253d4095a973c59ce63a30e540c2931b80d1654248e3a7271ef16f92c5ec534213877fcd5e0be82b242a6b93bb9ada6dd432df76a10c3f71c9
kss-espeo commented 3 years ago

Thanks @cgewecke , this works. However I would like to better understand why is that, since I need to implement this in Java (which has a poorer toolset for eth and there is no "magical" method to do what you suggested AFAIK) .

My main question is : how is a "raw transaction hash" different from a keccak hash of a raw transaction? I looked inside Transaction.getSenderPublicKey and a "raw transaction hash" it's using is this: 0x623997462cb089e708c99c886c1920eeba2d7fa13faeb0511b7b31b2028cea42 . However, a keccak hash of raw transaction (0xf86b0b85250523760082520894eafaf9bb8f35235d0df61275e86fd65d9ef2c3f9870aaa0065c66b8b8026a05fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15ba0121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c) is this: 0x26eed054f1ef51ae7a6c94841469c82a20f2deda84affde461b7d3887677bda8 .

If you could help me understand the difference between the two (perhaps direct me to something I can read to educate myself on that) , I could find some lib in Java that does that. I tried to read what happens inside Transaction.getMessageToVerifySignature , but it's a little too complex to understand.

cgewecke commented 3 years ago

how is a "raw transaction hash" different from a keccak hash of a raw transaction

The raw transaction hash is a transaction object which has been

[Edit: please see jochem brower's account of this in comments below which explains this process more clearly.]

I think Java methods equivalent to those implemented in @ethereumjs/tx can be found in web3j's crypto package.

jochem-brouwer commented 3 years ago

Hi @kss-espeo, a really interesting question, sat down to figure this out.

The confusion (at least from my side) indeed comes from this "raw tx hash" and the tx hash which you look up on etherscan. Realize that in the etherscan tx hash (so also the hash recorded in the blockchain as the "Transaction Hash") includes also the v, r and s values - but you can of course not know these values if you want to sign the message.

So, in order to solve this problem, you need to take the original transaction and actually hash that but exclude the v, r and s values. However you need to hash them in a rather specific way to get the raw tx hash. For reference see this code from @ethereumjs/tx. This code specifies how to get the "raw transaction hash" which you need to sign.

Below is an example script how to correctly get the hash from your specific transaction:

import { bnToRlp, BN, rlphash, toBuffer, unpadBuffer, ecrecover, pubToAddress } from "ethereumjs-util"

const nonce = new BN("11")
const gasPrice = new BN("159000000000") 
const gasLimit = new BN("21000")
const to = Buffer.from("eafaf9bb8f35235d0df61275e86fd65d9ef2c3f9", 'hex')
const value = new BN("3001668451330955")
const data = Buffer.from('')
const chainId = 1

const values = [
    bnToRlp(nonce),
    bnToRlp(gasPrice),
    bnToRlp(gasLimit),
    to !== undefined ? to : Buffer.from([]),
    bnToRlp(value),
    data
]

values.push(toBuffer(chainId))
values.push(unpadBuffer(toBuffer(0)))
values.push(unpadBuffer(toBuffer(0)))

const rawHash = rlphash(values)

const r = toBuffer('0x5fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15b');
const s = toBuffer('0x121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c');
const v = 0x26

const pubKey = ecrecover(rawHash, v, r, s, chainId);
const addr = pubToAddress(pubKey);

console.log(addr.toString('hex')) // 53b3779f4833116fcb87ebdbdccb61141eed7f87

You can of course also use @ethereumjs/tx and then use the getMessageToSign method.

jochem-brouwer commented 3 years ago

For clarity here's also the simpler implementation using @ethereumjs/tx

import { toBuffer, ecrecover, pubToAddress } from "ethereumjs-util"

import { Transaction } from '@ethereumjs/tx'

const Tx = Transaction.fromRlpSerializedTx(Buffer.from("f86b0b85250523760082520894eafaf9bb8f35235d0df61275e86fd65d9ef2c3f9870aaa0065c66b8b8026a05fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15ba0121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c", 'hex'))
const hash = Tx.getMessageToSign()

const r = toBuffer('0x5fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15b');
const s = toBuffer('0x121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c');
const v = 0x26

const chainId = 1

const pubKey = ecrecover(hash, v, r, s, chainId);
const addr = pubToAddress(pubKey);

console.log(addr.toString('hex')) // 53b3779f4833116fcb87ebdbdccb61141eed7f87

Note that this huge hex string is the raw transaction from etherscan

(This is the RLP-encoded version of the transaction).

cgewecke commented 3 years ago

Thanks @jochem-brouwer! That's a great answer.

kss-espeo commented 3 years ago

Thank you guys for the detailed answers. As far as I am concerned, @jochem-brouwer should be called Jochem Bro-U-Are ^^

I think the source of all confusion is two "raw" states of a transaction - before sign and after sign. It seems these two states are sort of called the same way, even by ETH node interfaces.

Please find a working Java solution below, should anybody need it in the future:

BigInteger v = new BigInteger("26", 16);
BigInteger r = new BigInteger("5fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15b", 16);
BigInteger s = new BigInteger("121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c", 16);
BigInteger chainId = new BigInteger("1", 16);
v = v.subtract(chainId.multiply(BigInteger.valueOf(2)).add(BigInteger.valueOf(8)));
Sign.SignatureData signatureData = new Sign.SignatureData(v.toByteArray(), r.toByteArray(), s.toByteArray());
byte[] raw = DatatypeConverter.parseHexBinary("f86b0b85250523760082520894eafaf9bb8f35235d0df61275e86fd65d9ef2c3f9870aaa0065c66b8b8026a05fd883bb01a10915ebc06621b925bd6d624cb6768976b73c0d468b31f657d15ba0121d855c539a23aadf6f06ac21165db1ad5efd261842e82a719c9863ca4ac04c");

RawTransaction decoded = TransactionDecoder.decode(DatatypeConverter.printHexBinary(raw));
byte[] encoded = TransactionEncoder.encode(decoded, chainId.longValue());
byte[] rawTxHash = Hash.sha3(encoded);

System.out.println("Raw tx hash:                    " + DatatypeConverter.printHexBinary(rawTxHash));
System.out.println("Pub key from raw tx hash :      " + signedMessageHashToKey(rawTxHash, signatureData).toString(16));

All the utility classes used come from web3j.crypto , as suggested by @cgewecke

cgewecke commented 3 years ago

Looks good @kss-espeo.