2

I'm currently trying to get the public key of the user that deploys a contract. Unfortunately I can't make it work.

I am trying to achieve this solely by using ethers.js as I don't want to bloat my React build with other packages. I can easily get the public key from a given signature using the following code taken from this issue.

    let msg = "This is a normal string.";
    let sig = await signer.signMessage(msg);
    const msgHash = ethers.utils.hashMessage(msg);
    const msgHashBytes = ethers.utils.arrayify(msgHash);
    const recoveredPubKey = ethers.utils.recoverPublicKey(msgHashBytes, sig);
    const recoveredAddress = ethers.utils.recoverAddress(msgHashBytes, sig);

When deploying a contract I should be able to do the same thing by simply stitching together the r, s and v values taken from the deployTransaction. The example in the documentation is similar. Here's my code:

    const deployTx = contract.deployTransaction;
    const msgHash = ethers.utils.hashMessage(deployTx.raw);
    const dataBytes = ethers.utils.arrayify(msgHash);
    const expanded = {
      r: deployTx.r,
      s: deployTx.s,
      recoveryParam: 0,
      v: deployTx.v
    };
    const signature = ethers.utils.joinSignature(expanded);

    // now the signature should be correctly formatted
    const recoveredPubKey = ethers.utils.recoverPublicKey(dataBytes, signature);
    const recoveredAddress = ethers.utils.recoverAddress(dataBytes, signature);

This approach does not work. As far as I know the data that was signed during the deployment is in deployTransaction.raw. So this should work. But I tested it with deployTransaction.data as well.

To me it looks like the signature might be wrong. The joinSignature automatically converts the v value to either 27 or 28. According to EIP155 this doesn't make any sense?

Edit: To clarify, I think all I need is the true signing hash. How can I generate it? It's apparently not the hash of the raw deployment transaction.

Edit 2: After some research in the ethereum book I found this:

In Ethereum’s implementation of ECDSA, the "message" being signed is the transaction, or more accurately, the Keccak-256 hash of the RLP-encoded data from the transaction. The signing key is the EOA’s private key.

So I changed my code to the following:

    const deployTx = contract.deployTransaction;
    const msg = ethers.utils.RLP.encode(deployTx.data);
    const msgHash = ethers.utils.keccak256(msg);
    const msgBytes = ethers.utils.arrayify(msgHash);
    const expanded = {
      r: deployTx.r,
      s: deployTx.s,
      recoveryParam: 0,
      v: deployTx.v
    };
    const signature = ethers.utils.joinSignature(expanded);
    const recoveredPubKey = ethers.utils.recoverPublicKey(
      msgBytes,
      signature
    );
    const recoveredAddress = ethers.utils.recoverAddress(msgBytes, signature);

This still does not work unfortunately.

Chris
  • 1,302
  • 12
  • 23
  • Depending if EIP155 applies or not, you have to remove r, s, v from the for the message to be signed or replace them by zeros. Look a this typescript implementation https://github.com/ethereumjs/ethereumjs-tx/blob/v2.1.2/src/transaction.ts#L182-L192 (to sign it is called tx.hash(false)). – Ismael Jan 10 '20 at 20:22

2 Answers2

4

This is solved now. There was a tiny bug in the code and in the ethers library that would not correctly return the chainId and calculate the v value. It's fixed now, see here. Many thanks to ricmoo for helping out.

I wrote a gist that correctly recovers the public key given the transaction.

In short:

const tx = await provider.getTransaction(...)
const expandedSig = {
  r: tx.r,
  s: tx.s,
  v: tx.v
}
const signature = ethers.utils.joinSignature(expandedSig)
const txData = {
  gasPrice: tx.gasPrice,
  gasLimit: tx.gasLimit,
  value: tx.value,
  nonce: tx.nonce,
  data: tx.data,
  chainId: tx.chainId,
  to: tx.to // you might need to include this if it's a regular tx and not simply a contract deployment
}
const rsTx = await ethers.utils.resolveProperties(txData)
const raw = ethers.utils.serializeTransaction(rsTx) // returns RLP encoded tx
const msgHash = ethers.utils.keccak256(raw) // as specified by ECDSA
const msgBytes = ethers.utils.arrayify(msgHash) // create binary hash
const recoveredPubKey = ethers.utils.recoverPublicKey(msgBytes, signature)
const recoveredAddress = ethers.utils.recoverAddress(msgBytes, signature)
Chris
  • 1,302
  • 12
  • 23
  • This is really great. For some reason, it is not working for me tho. The address generated is not correct. I am trying to do this with the transaction on mumbai-polygon 0xa0933a136d9d1166defc5f841089055a9f94208704b0fcbed618713aa3d6a4bd which was made by the address 0xdb326d217c2452faebb40aceb75000f0bb986703, but the algorithm gets ``` – Oscar Serna Aug 21 '22 at 23:18
  • 0xD4969Eb27CE47013058Ed27ac2210C4dce5CE111 – Oscar Serna Aug 21 '22 at 23:43
2

I've updated answer by Chris and put it into a function. Confirmed to be working correctly in my code.

import { ethers } from "ethers";

export async function recover(tx: ethers.Transaction): Promise<{ publicKey: string; address: string; }> { const expandedSig = { r: tx.r!, s: tx.s!, v: tx.v!, };

const signature = ethers.utils.joinSignature(expandedSig);

const txData = { gasLimit: tx.gasLimit, value: tx.value, nonce: tx.nonce, data: tx.data, chainId: tx.chainId, to: tx.to, // you might need to include this if it's a regular tx and not simply a contract deployment type: tx.type, maxFeePerGas: tx.maxFeePerGas, maxPriorityFeePerGas: tx.maxPriorityFeePerGas, };

const rsTx = await ethers.utils.resolveProperties(txData); const raw = ethers.utils.serializeTransaction(rsTx); // returns RLP encoded tx const msgHash = ethers.utils.keccak256(raw); // as specified by ECDSA const msgBytes = ethers.utils.arrayify(msgHash); // create binary hash

return { publicKey: ethers.utils.recoverPublicKey(msgBytes, signature), address: ethers.utils.recoverAddress(msgBytes, signature), }; }

Vlad Faust
  • 71
  • 5
  • Thanks, I executed the above code and it gave me a hash starting with 0x for the public key, but the public key I get is 132 characters. Am I doing something wrong? because I read that Ethereum public keys are 64 characters. – m0j1 Feb 04 '23 at 12:55