1

I would like to recover the public key (or address) from the r,s and v values of a signed transaction without using a private key. The idea of course being, that I want to use Nick's method to deploy a contract registry at a deterministic address.

This forum post (or rather the answer) uses ethers-js which seems to automatically try to comply with EIP-155 (adding a chainId to the transaction). Instead I tried using the ecrecover method. The ethereumtx-util library has a ecrecover implementation which uses the arguments v, s, r and a 32-byte message. So immediately I thought this must be the transaction hash but I got the notification that the public key could not be recovered. What should I put here as the message? Are there better ways to recover the public key?

For those interested here is my code (The variable DeploymentByteCode holds a string with valid deployment code)

import {ethers} from "hardhat";
import {Transaction} from 'ethereumjs-tx'
import {bufferToHex, pubToAddress, ecrecover, sha256} from 'ethereumjs-util'

let GasPrice = ethers.utils.formatUnits("100","gwei")
let GasLimit = ethers.utils.formatUnits("100000","wei") // in abuse of (wei-)notation

async function recover(){

  const expandedSig = {
    r: '0xa0de33f4f398c1f5f9bce68825cbc1afd8078ee95bba17f5ba9e8ed046ef7de4',
    s: '0x34290374099df8f0b7f1702de210094fef777428563814560202ec8aff531016',
    v: 28
  }
  

  let txData = {
    gasPrice: GasPrice.toHexString(),
    gasLimit: GasLimit.toHexString(),
    to: undefined,
    value: ZeroBigNumber.toHexString(),
    nonce: 0,
    data: DeploymentByteCode
  }

  let signedTxData = {
    ...txData,
    ...expandedSig
  }


  const tx = new Transaction(signedTxData,  {hardfork: 'homestead' });
  const serializedTx = tx.serialize()
  let serializedTxToHexNoPrefix = tx.serialize().toString('hex')
  let bufferedtxNoPrefix = Buffer.from(serializedTxToHexNoPrefix, 'hex')
  let txHashComputedNoPrefix = ethers.utils.keccak256(bufferedtxNoPrefix)
  let bufferedMsg = Buffer.from(txHashComputedNoPrefix.slice(2,), 'hex')

  const pubKey  = ecrecover(
    bufferedMsg,
    expandedSig.v,
    Buffer.from(expandedSig.r, 'hex'),
    Buffer.from(expandedSig.s, 'hex'),
    0);
  const addrBuf = pubToAddress(pubKey);
  const recoveredAddress = bufferToHex(addrBuf);
  console.log("recoveredAddress", recoveredAddress)
}

recover()

The error I get is (note also the error recover => recovered in the error statement):

Error: Public key could not be recover

Note: I have another post that deals with the first part of this question regarding the correct form of the message that is required to be passed into the ecrecover function. Answering that would make a little bit of progress on this question, too:

1
  • Okay, I understand so far that the args of ecrecover should be the unsigned message and the signature (ie r,s & v). However, if I use the unsigned txData and calculate the RLP, serialized and hash it, I should recover r,s and v values. But I dont const unsignedTx = new Transaction(txData, {hardfork: 'homestead' }); const serializedTx = tx.serialize(); const unsignedTxHash = ethers.utils.keccak256(Buffer.from(unsignedTx.serialize().toString('hex'),'hex')); let ethersSignature = await ALICE.signMessage(unsignedTxHash) Guess I gotta go through the exercise of rlp-ing it by myself.
    – Marlo
    Commented Apr 11, 2022 at 9:06

2 Answers 2

2

I just spent 3-4 frustrating hours on this same issue when i came across your question, and since i woudn't wish that kind of frustration on anyone, decided to create an account just to post this ... came across this github gist after hours of toiling, and it worked magic, hope it helps you as it did me.

Here goes your answer(hopefully)

PS: You vets should cut me some slack on how or how-not to post answers, don't stress me out the more folks ...

Here's my own refactored solution:

const ethers = require("ethers");
exports.getPublicKeyFromTransactionID = async (transactionHash) => {

// const moralisProvider = new ethers.providers.JsonRpcProvider(moralis_rinkeby_provider);
// console.log(transactionHash);
const expandedSig = {
    r: transactionHash.r,
    s: transactionHash.s,
    v: transactionHash.v
}

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

let transactionHashData;
switch (transactionHash.type) {
    case 0:
        transactionHashData = {
            gasPrice: transactionHash.gasPrice,
            gasLimit: transactionHash.gasLimit,
            value: transactionHash.value,
            nonce: transactionHash.nonce,
            data: transactionHash.data,
            chainId: transactionHash.chainId,
            to: transactionHash.to
        };
        break;
    case 2:
        transactionHashData = {
            gasLimit: transactionHash.gasLimit,
            value: transactionHash.value,
            nonce: transactionHash.nonce,
            data: transactionHash.data,
            chainId: transactionHash.chainId,
            to: transactionHash.to,
            type: 2,
            maxFeePerGas: transactionHash.maxFeePerGas,
            maxPriorityFeePerGas: transactionHash.maxPriorityFeePerGas
        }
        break;
    default:
        throw "Unsupported transactionHash type";
}
const rstransactionHash = await ethers.utils.resolveProperties(transactionHashData)
const raw = ethers.utils.serializeTransaction(rstransactionHash) // returns RLP encoded transactionHash
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)
return recoveredPubKey
}
2

If the answer with ethersjs does not work, you can try with ethereumjs for 1559 txs.

import { Chain, Hardfork } from '@ethereumjs/common'
import {default as common} from '@ethereumjs/common';
const Common = common.default
import txjs from '@ethereumjs/tx'
import ethereumUtil, { toBuffer } from 'ethereumjs-util';

const c = new Common({ chain: Chain.Mainnet})

//hash of tx - 0x58518aae7f8027afc42d34dcb883f1a8e048d379f72e5215cae192b72f765a6b

const txData = {
  "gasLimit": '0x2728e',
  "value": '0x0',
  "nonce": '0x3',
  "data": '0x8b9e4f930000000000000000000000007d1afa7b718fb893db30a3abc0cfc608aacfebb0000000000000000000000000fed23c6413514a71bcb265e87de49e8255db94920000000000000000000000000000000000000000000000065988bcbaf7508000',
  "chainId": "0x01",
  "type": "0x02",
  "maxFeePerGas": '0xb5f0fe565',
  "maxPriorityFeePerGas": '0x452b8e9b',
  "v":"0x1",
  "r":"0x4cc425dcbe5db936cf9f1ed373cf1ec07da58d682f5839bf8d95ebc616ab1e14",
  "s":"0x186f9d18c2ab8261389b7fda7a2330b301031e6a39d1a9c4f712204fb60a89a",
  "to": '0x401f6c983ea34274ec46f84d70b31c151321188b',
}

const tx = txjs.FeeMarketEIP1559Transaction.fromTxData(txData, { c })

// get a public key of sender
const publicKey = ethereumUtil.bufferToHex(tx.getSenderPublicKey())
console.log("rec ethereumjs pubkey: ", publicKey);
// Get an address of sender
const address = ethereumUtil.bufferToHex(tx.getSenderAddress())
console.log("rec ethereumjs address:", address);

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.