Raw Ethereum Transaction – How to Construct a Raw Ethereum Transaction from Transaction Data

ethers.jsjson-rpcraw-transactionweb3.pyweb3js

Let's say I have the following transaction information:

{
  "jsonrpc":"2.0"
  "id":0
  "result":{
    "blockHash":"0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229"
    "blockNumber":"0xa4f285"
    "from":"0xea8a7ef30f894bce23b42314613458d13f9d43ea"
    "gas":"0x30d40"
    "gasPrice":"0x2e90edd000"
    "hash":"0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511"
    "input":"0x"
    "nonce":"0x1e7"
    "to":"0xbd064928cdd4fd67fb99917c880e6560978d7ca1"
    "transactionIndex":"0x0"
    "value":"0xde0b6b3a7640000"
    "v":"0x25"
    "r":"0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f"
    "s":"0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995"
  }
}

Along with anything else you may need about the network broadly (such as chain_id).

Is it possible to construct an analogous raw transaction from this information without knowing the private key of the sender?

Feel free to use your favorite Web3 SDK.

Best Answer

A raw transaction is just all the transaction parameters encoded in the RLP format. A raw transaction consists of (in this order):

  • Nonce;
  • Gas price;
  • Gas limit;
  • To address;
  • Value;
  • Data.

You don't need the private key of the sender: to get the raw transaction, you can simply encode it using RLP(Nonce, Gas Price, Gas limit, To, Value, Data). Here's an example implementation in JavaScript (using ethereumjs/rlp):

import { encode } from 'rlp';

const transaction = {
  nonce: '0x1e7',
  gasPrice: '0x2e90edd000',
  gasLimit: '0x30d40',
  to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
  value: '0xde0b6b3a7640000',
  data: '0x'
};

// Raw transaction as Buffer
const rawTransaction = encode([transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to, transaction.value, transaction.data]);
console.log(rawTransaction.toString('hex'));

This results in the following raw transaction:

ec8201e7852e90edd00083030d4094bd064928cdd4fd67fb99917c880e6560978d7ca1880de0b6b3a764000080

The only part where a private key is required, is to sign the transaction. A signed transaction is basically the raw transaction with the v, r and s signature (of the raw transaction) added to it: RLP(Nonce, Gas Price, Gas limit, To, Value, Data, v, r, s). The chain ID is encoded in the v parameter (see EIP-155).

Here's an example of how to sign the transaction and get the signed transaction:

import { randomBytes } from 'crypto';
import { ecsign, keccak256 } from 'ethereumjs-util';
import { encode } from 'rlp';

// Randomly generated 32 byte private key
const privateKey = randomBytes(32);

const transaction = {
  nonce: '0x1e7',
  gasPrice: '0x2e90edd000',
  gasLimit: '0x30d40',
  to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
  value: '0xde0b6b3a7640000',
  data: '0x',
  chainId: 1
};

const params = [transaction.nonce, transaction.gasPrice, transaction.gasLimit, transaction.to, transaction.value, transaction.data];

// Raw transaction as Buffer
const rawTransaction = encode(params);
const hash = keccak256(rawTransaction);

// Signature of the hash of the raw transaction
const { v, r, s } = ecsign(hash, privateKey, transaction.chainId);

// RLP encoded signed transaction as Buffer
const signedTransaction = encode([...params, v, r, s]);
console.log(signedTransaction.toString('hex'));

Note the extra chainId property of transaction, this is required to get the correct value for v. If we assume our signature is the one mentioned in the example transaction, the signed transaction looks like this:

f86f8201e7852e90edd00083030d4094bd064928cdd4fd67fb99917c880e6560978d7ca1880de0b6b3a76400008025a07e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093fa049634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995

This is the full transaction that can be broadcast to the network. You can verify this through MyCrypto's broadcast tool for example.