Closed kss-espeo closed 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
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.
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.
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.
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).
Thanks @jochem-brouwer! That's a great answer.
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
Looks good @kss-espeo.
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:
It prints a following output:
I suppose this means I am using erecover incorrectly. So what am I doing wrong?