5

I'm trying to find out how to derive a public key (not the address) of an Ethereum account by using ethereumjs-util's ecrecover method.

Basically I get a transaction by hash using Web3, convert r, s and v like in this answer, and pass them to ecrecover. It does yield a public key, but that key does not correspond to the address of the sender of the transaction.

What am I doing wrong?

Code below:

const ethJsUtil = require('ethereumjs-util');

class EthPubKeyReg {

    constructor(web3, chainId) {
        this.chainId = chainId;
        this.web3 = web3;
    }

    async extractPubKey(txHash, chainId) {
        chainId = (chainId || this.chainId);
        let tx = await this.web3.eth.getTransaction(txHash);

        let v = parseInt(tx.v);

        let msgHash = ethJsUtil.toBuffer(tx.hash);
        let r = ethJsUtil.toBuffer(tx.r);
        let s = ethJsUtil.toBuffer(tx.s);

        let pubKey = ethJsUtil.ecrecover(msgHash, v, r, s);


        console.log("address", ethJsUtil.bufferToHex(ethJsUtil.pubToAddress(pubKey)));
        return pubKey;
    }

    async extractPubKeyString(txHash, chainId) {
        return ethJsUtil.bufferToHex(await this.extractPubKey(txHash, chainId));
    }
}

===== Update =====

I've updated my code the way @libertylocked suggested in his answer below:

const Web3 = require('web3');
const ethJsUtil = require('ethereumjs-util');
const Transaction = require('ethereumjs-tx');

const HDWalletProvider = require('truffle-hdwallet-provider');
let provider = new HDWalletProvider(
    'some words to instantiate this truffe hdwallet provider',
    "https://mainnet.infura.io/[ACCCESS TOKEN]"
);

let web3 = new Web3(provider);

/* 
*  Extracts the public key of the sender of a 
*  signed and mined transaction as Buffer
*/
async function extractPubKey(txHash, web3) {

    const tx = await web3.eth.getTransaction(txHash) // insert txhash here
    console.log("tx.from", tx.from);

    const txDetails = new Transaction({
        nonce: tx.nonce,
        gasPrice: `0x${tx.gasPrice.toString(16)}`,
        gasLimit: tx.gas,
        to: tx.to,
        value: `0x${tx.value.toString(16)}`,
        data: tx.input,
        chainId: web3.version.network,
        r: tx.r,
        s: tx.s,
        v: tx.v,
    });
    const pubkey = txDetails.getSenderPublicKey()
    console.log("txDetails.getSenderAddress", txDetails.getSenderAddress().toString('hex'));
    console.log("Pubkey to address", ethJsUtil.bufferToHex(ethJsUtil.pubToAddress(pubkey)));
    return pubkey;
}

/* 
 *  Extracts the public key of the sender of a 
 *  signed and mined transaction as hex string  
 */
async function extractPubKeyAsString(txHash, web3) {
    return ethJsUtil.bufferToHex(await extractPubKey(txHash, web3));
}

// link to tx on Etherscan: https://etherscan.io/tx/0xbee6f2a54e1c4921dec2d6834334acd3177ce42229e1d1c4d2490fb60839634e
extractPubKeyAsString('0xbee6f2a54e1c4921dec2d6834334acd3177ce42229e1d1c4d2490fb60839634e', web3)
    .then(pubkey => {
        console.log("pubkeystring", pubkey);
    })
    .finally(() => process.exit());

However, this still yields an incorrect sender address:

tx.from 0xBa34A08bcAe1AB74b5e52b6eE06b108945A387f0
txDetails.getSenderAddress efc92668485027e2d41bd229d66111ad0aaa8a16
Pubkey to address 0xefc92668485027e2d41bd229d66111ad0aaa8a16
pubkeystring 0x6d5441c4e53bb8c4dd69b16e79d5ca4f51a84eebaded3b6dfc5b51dd63ee7b5c5707ca1e788653b07872dd8c13293be840c93aeae582f2c080ff8a3aef79a24f
3
+50

The easiest solution is to use ethereumjs-tx module.

If you use web3@0.20:

const Transaction = require('ethereumjs-tx')
const tx = web3.eth.getTransaction(txHash) // insert txhash here
const pubkey = new Transaction({
  nonce: tx.nonce,
  gasPrice: `0x${tx.gasPrice.toString(16)}`,
  gasLimit: tx.gas,
  to: tx.to,
  value: `0x${tx.value.toString(16)}`,
  data: tx.input,
  chainId: 1, // mainnet network ID is 1. or use web3.version.network to find out
  r: tx.r,
  s: tx.s,
  v: tx.v,
}).getSenderPublicKey()

console.log(pubkey.toString('hex'))

If you use web3 1.0 beta (and you use ethereumjs-util), you need to convert some values to 0x prefixed hex strings

new Transaction({
    nonce: tx.nonce,
    gasPrice: ethJsUtil.bufferToHex(new ethJsUtil.BN(tx.gasPrice)),
    gasLimit: tx.gas,
    to: tx.to,
    value: ethJsUtil.bufferToHex(new ethJsUtil.BN(tx.value)),
    data: tx.input,
    chainId: 1,
    r: tx.r,
    s: tx.s,
    v: tx.v,
});

To verify if the pub key can be derived to the same sender address you can either use publicToAddress(pubkey) from ethereumjs-util, or use tx.getSenderAddress() from ethereumjs-tx

  • The public key this yields does not seem to correspond to the transaction sender's address – Henk Sep 20 '18 at 7:43
  • Have you tried getSenderAddress().toString('hex')? It should match tx.from. – libertylocked Sep 20 '18 at 9:08
  • I'm sorry, that yields the same incorrect address – Henk Sep 20 '18 at 9:16
  • Have you pre-pended 0x? Otherwise try verifySignature() on the ethereumjs tx object – libertylocked Sep 20 '18 at 9:23
  • I've updated my question to show how I'm testing the correctness – Henk Sep 20 '18 at 9:40

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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