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.

1
  • 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 github.com/ethereumjs/ethereumjs-tx/blob/v2.1.2/src/… (to sign it is called tx.hash(false)).
    – Ismael
    Commented Jan 10, 2020 at 20:22

2 Answers 2

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)
2
  • 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 ``` Commented Aug 21, 2022 at 23:18
  • 0xD4969Eb27CE47013058Ed27ac2210C4dce5CE111 Commented Aug 21, 2022 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),
  };
}
1
  • 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
    Commented Feb 4, 2023 at 12:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.