Ethers.js: How to recover the public key and address from a signed message?

Created on 7 Mar 2019  路  11Comments  路  Source: ethers-io/ethers.js

The following sample is returning a different address from the signed message

const { ethers } = require('ethers')
;(async function() {
  // yarn ganache-cli -p 8545 -d
  const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545')
  const signer = provider.getSigner()

  // 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
  const ethAddress = await signer.getAddress()
  const hash = await ethers.utils.keccak256(ethAddress)
  const sig = await signer.signMessage(ethers.utils.arrayify(hash))

  // 0x3dfB86Ee0e620A3E5B88195792e1B1f8B0A08cff
  const recoveredAddress = ethers.utils.recoverAddress(hash, sig)

  // Throwing here
  if (recoveredAddress != ethAddress) {
    throw Error(
      `Address recovered do not match, original ${ethAddress} versus computed ${recoveredAddress}`
    )
  }
})()

Expecting to return 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1

Node: Using ganache-cli to test yarn ganache-cli -p 8545 -d

discussion

Most helpful comment

In case someone finds it useful, here is our example:

    // Issue a signature
    const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
    const message = "Hello dapp"
    const signature = await wallet.signMessage(message)
    const expectedAddress = await wallet.getAddress()
    const expectedPublicKey = wallet.signingKey.publicKey

    console.log("ISSUING SIGNATURE")
    console.log("ADDR:    ", expectedAddress)
    console.log("PUB K:   ", expectedPublicKey)
    console.log("SIG      ", signature)
    console.log()

    // Approach 1
    const actualAddress = utils.verifyMessage(message, signature)

    console.log("APPROACH 1")
    console.log("EXPECTED ADDR: ", expectedAddress)
    console.log("ACTUAL ADDR:   ", actualAddress)
    console.log()

    // Approach 2
    const msgHash = utils.hashMessage(message);
    const msgHashBytes = utils.arrayify(msgHash);

    // Now you have the digest,
    const recoveredPubKey = utils.recoverPublicKey(msgHashBytes, signature);
    const recoveredAddress = utils.recoverAddress(msgHashBytes, signature);

    const matches = expectedPublicKey === recoveredPubKey

    console.log("APPROACH 2")
    console.log("EXPECTED ADDR:    ", expectedAddress)
    console.log("RECOVERED ADDR:   ", recoveredAddress)

    console.log("EXPECTED PUB K:   ", expectedPublicKey)
    console.log("RECOVERED PUB K:  ", recoveredPubKey)

    console.log("SIGNATURE VALID:  ", matches)
    console.log()

I think that the documentation is missing an example about this use-case.

All 11 comments

The recoverAddress works on digests, and is for much lower-level functionality. You probable want verifyMessage, which works on messages:

// Make sure you arrayify the message if you want the bytes to be used as the message
const recoveredAddress = ethers.utils.verifyMessage(ethers.utils.arrayify(hash), sig)

Let me know if that still doesn't work.

Hey @ricmoo thanks for the answer, actually I'm looking for return the public key which, when returning the public key, the generated address isn't the one I expect.

const { ethers } = require('ethers')
;(async function() {
  // yarn ganache-cli -p 8545 -d
  const provider = new ethers.providers.JsonRpcProvider('http://localhost:8545')
  const signer = provider.getSigner()

  // 0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1
  const ethAddress = await signer.getAddress()
  const hash = await ethers.utils.keccak256(ethAddress)
  const sig = await signer.signMessage(ethers.utils.arrayify(hash))
  const pk = ethers.utils.recoverPublicKey(hash, sig)

  // 0x3dfB86Ee0e620A3E5B88195792e1B1f8B0A08cff
  const recoveredAddress = ethers.utils.computeAddress(ethers.utils.arrayify(pk))
  // Throwing here
  if (recoveredAddress != ethAddress) {
    throw Error(
      `Address recovered do not match, original ${ethAddress} versus computed ${recoveredAddress}`
    )
  }
})()

Also verifyMessage is working and returning the address correctly, but I really need the right publicKey

And the pk generated doesn't generate the publickKey that I expected, because the address computed isn't the right one

In that case you can use the lower level, but you must use hashMessage first:

// First the message must be binary
let hashBytes = arrayify(hash);

// Then you must compute the prefixed-message hash
let messgeHash = ethers.util.hashMessage(hashBytes);

// Then you must make this binary
let messageHashBytes = ethers.utils.arrayify(messageHash);

// Now you have the digest,
let publicKey = ethers.utils.recoverPublicKey(messageHashBytes);

You can of course make this a one liner:

let pubKey = recoverPublicKey(arrayify(hashMessage(arrayify(hash))), signature);
let address = computeAddress(pubKey)

That should get you where you want. :)

Sweet ! now is working as expected thank you very much :)

  const pk = ethers.utils.recoverPublicKey(
    ethers.utils.arrayify(ethers.utils.hashMessage(ethers.utils.arrayify(hash))),
    sig
  )

In case someone finds it useful, here is our example:

    // Issue a signature
    const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
    const message = "Hello dapp"
    const signature = await wallet.signMessage(message)
    const expectedAddress = await wallet.getAddress()
    const expectedPublicKey = wallet.signingKey.publicKey

    console.log("ISSUING SIGNATURE")
    console.log("ADDR:    ", expectedAddress)
    console.log("PUB K:   ", expectedPublicKey)
    console.log("SIG      ", signature)
    console.log()

    // Approach 1
    const actualAddress = utils.verifyMessage(message, signature)

    console.log("APPROACH 1")
    console.log("EXPECTED ADDR: ", expectedAddress)
    console.log("ACTUAL ADDR:   ", actualAddress)
    console.log()

    // Approach 2
    const msgHash = utils.hashMessage(message);
    const msgHashBytes = utils.arrayify(msgHash);

    // Now you have the digest,
    const recoveredPubKey = utils.recoverPublicKey(msgHashBytes, signature);
    const recoveredAddress = utils.recoverAddress(msgHashBytes, signature);

    const matches = expectedPublicKey === recoveredPubKey

    console.log("APPROACH 2")
    console.log("EXPECTED ADDR:    ", expectedAddress)
    console.log("RECOVERED ADDR:   ", recoveredAddress)

    console.log("EXPECTED PUB K:   ", expectedPublicKey)
    console.log("RECOVERED PUB K:  ", recoveredPubKey)

    console.log("SIGNATURE VALID:  ", matches)
    console.log()

I think that the documentation is missing an example about this use-case.

@ledfuse Thanks, this helped me a lot.

Found a bug. When signing UTF-string, recoverPublicKey and recoverAddress derive invalid pkey and addr.

Should I create another issue for this?

    const provider = new ethers.providers.Web3Provider(window.web3.currentProvider);
    const signer = provider.getSigner();

    const message = '邪斜胁谐写械褢';
    const signature = await signer.signMessage(message);
    const digest = arrayify(hashMessage(message));
    const publicKey = await recoverPublicKey(digest, signature);
    const address = await recoverAddress(digest, signature);

    const message2 = 'hijklmnop';
    const signature2 = await signer.signMessage(message2);
    const digest2 = arrayify(hashMessage(message2));
    const publicKey2 = await recoverPublicKey(digest2, signature2);
    const address2 = await recoverAddress(digest2, signature2);

    console.log(`Addresses: 1 - ${address}; 2 - ${address2}`);
    console.log(`Pubkeys: 1 - ${publicKey}; 2 - ${publicKey2}`);

@seniorjoinu Sorry, I didn't notice this message.

I just tried in both v4 and v5, and in both versions the code you provided works fine (they match). Can you give an example with an ethers.Wallet instance and private key and include the output you get so I can reproduce it deterministically?

(also, feel free to open a new issue ;))

In case someone finds it useful, here is our example:

    // Issue a signature
    const wallet = Wallet.fromMnemonic(MNEMONIC, PATH)
    const message = "Hello dapp"
    const signature = await wallet.signMessage(message)
    const expectedAddress = await wallet.getAddress()
    const expectedPublicKey = wallet.signingKey.publicKey

    console.log("ISSUING SIGNATURE")
    console.log("ADDR:    ", expectedAddress)
    console.log("PUB K:   ", expectedPublicKey)
    console.log("SIG      ", signature)
    console.log()

    // Approach 1
    const actualAddress = utils.verifyMessage(message, signature)

    console.log("APPROACH 1")
    console.log("EXPECTED ADDR: ", expectedAddress)
    console.log("ACTUAL ADDR:   ", actualAddress)
    console.log()

    // Approach 2
    const msgHash = utils.hashMessage(message);
    const msgHashBytes = utils.arrayify(msgHash);

    // Now you have the digest,
    const recoveredPubKey = utils.recoverPublicKey(msgHashBytes, signature);
    const recoveredAddress = utils.recoverAddress(msgHashBytes, signature);

    const matches = expectedPublicKey === recoveredPubKey

    console.log("APPROACH 2")
    console.log("EXPECTED ADDR:    ", expectedAddress)
    console.log("RECOVERED ADDR:   ", recoveredAddress)

    console.log("EXPECTED PUB K:   ", expectedPublicKey)
    console.log("RECOVERED PUB K:  ", recoveredPubKey)

    console.log("SIGNATURE VALID:  ", matches)
    console.log()

I think that the documentation is missing an example about this use-case.

Please add this to documentation.

Thank you @MoMannn. You ended a very miserable couple of hours for me

Was this page helpful?
0 / 5 - 0 ratings