Ethers.js – Recover Public Key from Contract Deployment via v,r,s Values

contract-deploymentecrecoverethers.jspublic-key

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.

Best Answer

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)