Decoding Ethereum Transactions: RLP, Selectors and ABI Calldata

Read a raw signed transaction byte by byte, work out a function selector, decode ABI calldata with dynamic types, and build explorer links that resolve.

ยท 8 min read

Screenshot of RedBison's Ethereum Transaction Decoder running in the browser

A hardware wallet shows "Contract interaction, data: 0x095ea7b3..." and asks you to confirm. Those ten characters already tell you most of what matters: 0x095ea7b3 is the selector for approve(address,uint256), so you are about to let some address spend your tokens, and the next 64 bytes say which address and how much. Reading Ethereum transactions is mostly a matter of knowing where the bytes go. This guide walks through a raw signed transaction, then the calldata inside it, then how to get from a hash to the right explorer page.

What a raw transaction contains

A signed transaction is a short byte string. Wallets call it the raw or serialized transaction, and a node will return it for a known hash through eth_getRawTransactionByHash. It is not JSON and has no field names. It is RLP, Recursive Length Prefix encoding, which only knows two things: byte strings and lists of items.

RLP rules fit in a few lines:

  • A single byte below 0x80 is itself.
  • A string of 0 to 55 bytes is 0x80 + length, then the bytes. So 0x82 5208 is the two-byte string 0x5208, which is 21000.
  • A list whose contents total 0 to 55 bytes is 0xc0 + length, then the items. Longer strings and lists use 0xb7 or 0xf7 plus the length of the length.
  • Integers are big-endian with no leading zeros, and zero is the empty string 0x80.

A legacy transaction is an RLP list of nine items: nonce, gasPrice, gasLimit, to, value, data, v, r, s. The first byte, usually 0xf8 or 0xf9, is the list prefix.

Since the Berlin upgrade in 2021, EIP-2718 lets a transaction start with a type byte below 0x80 instead, followed by a type-specific RLP list. That is the whole trick for telling them apart.

First byteTypeFields after the type byte
0xc0 and up (often 0xf8)Legacynonce, gasPrice, gasLimit, to, value, data, v, r, s
0x01EIP-2930chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, yParity, r, s
0x02EIP-1559chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, yParity, r, s
0x03EIP-4844 blobthe EIP-1559 fields plus maxFeePerBlobGas and blobVersionedHashes
0x04EIP-7702the EIP-1559 fields plus authorizationList
Diagram of an EIP-1559 raw transaction: the 0x02 type byte followed by an RLP list of chainId, nonce, priority fee, max fee, gas limit, to, value, data, access list and signature fields
Diagram of an EIP-1559 raw transaction: the 0x02 type byte followed by an RLP list of chainId, nonce, priority fee, max fee, gas limit, to, value, data, access list and signature fields

Worked example with the decoder

EIP-155 publishes a reference signed transaction. Paste it into the Ethereum Transaction Decoder and press Decode Transaction:

0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83

Reading it by hand matches what the decoder reports:

BytesMeaning
f86cList of 0x6c = 108 bytes
09nonce 9
85 04a817c800gasPrice 20,000,000,000 wei = 20 gwei
82 5208gasLimit 21000
94 3535...35to, a 20-byte address
88 0de0b6b3a7640000value 10^18 wei = 1 ETH
80data, empty
25v = 37
a0 28ef... / a0 67cb...r and s, 32 bytes each

The v value carries the chain. EIP-155 sets v = chainId x 2 + 35 or + 36, so 37 means chain 1, Ethereum mainnet. A v of 27 or 28 would mean the transaction has no chain ID and could be replayed on any chain that accepts it. Typed transactions skip this trick and store chainId as their first field, with a plain yParity of 0 or 1 instead of v.

The decoder also prints the transaction hash, 0x33469b22...0788. That hash is Keccak-256 over the raw bytes, type byte included. It is a fingerprint, which is why you cannot paste a hash into a decoder and get the fields back. Paste a 66-character hash and the tool says exactly that and offers an explorer link instead.

The one field you will not see is the sender. Ethereum transactions do not contain a from address. Nodes recover it from r, s and v using secp256k1 public key recovery, and the decoder does not do that step.

Function selectors and calldata

The data field is where contract calls live. Its first four bytes are the function selector: the first four bytes of keccak256 of the canonical signature.

keccak256("transfer(address,uint256)") = 0xa9059cbb2ab09eb2...
selector                               = 0xa9059cbb

Canonical means the function name, then the parameter types separated by commas, with no spaces, no parameter names and no aliases. uint must be written uint256. A struct parameter becomes a tuple of its member types in parentheses, so a function taking an array of (address, uint256) structs is hashed as swap((address,uint256)[],bytes). Most "my selector doesn't match" bugs are one of these: a space, a uint instead of uint256, or a struct name instead of its tuple.

Events use the same hash in full. topic0 for Transfer(address,address,uint256) is 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef on every ERC-20 and ERC-721 contract, and the indexed keyword does not change it.

After the selector come the arguments, encoded in 32-byte words. Static types sit in place: an address is left-padded to 32 bytes, a uint256 is the number itself, a bool is 0 or 1. Dynamic types (bytes, string and T[]) are split into a head and a tail. The head slot holds an offset, counted from the start of the arguments, and the tail at that offset begins with a 32-byte length followed by the data, padded to a multiple of 32.

Diagram of ABI calldata for sam(bytes,bool,uint256[]): a 4-byte selector, three head words where the first and third are offsets 0x60 and 0xa0, and the tail holding the length and data for each dynamic argument
Diagram of ABI calldata for sam(bytes,bool,uint256[]): a 4-byte selector, three head words where the first and third are offsets 0x60 and 0xa0, and the tail holding the length and data for each dynamic argument

The Solidity documentation uses sam(bytes,bool,uint256[]) called with ("dave", true, [1, 2, 3]) as its example. After selector 0xa5643bf2 the words are: 0x60 (offset to the bytes), 1 (true), 0xa0 (offset to the array), then at 0x60 the length 4 and "dave", then at 0xa0 the length 3 and the values 1, 2, 3. The Smart Contract ABI Decoder follows exactly these offsets, so you can paste that calldata with a matching ABI and get the three values back.

Decoding a real call

Paste an ERC-20 ABI into the ABI Input box, and this into Calldata to decode:

0xa9059cbb0000000000000000000000005aaeb6053f3e94c9b9a09f33669435e7ef1beaed00000000000000000000000000000000000000000000000000000000000f4240

Press Decode ABI. The report starts with:

- Function: transfer(address,uint256)
- Selector: 0xa9059cbb
- to (address): 0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed
- amount (uint256): 1000000

The amount is in base units. For USDC, with 6 decimals, that is exactly 1 token. For an 18-decimal token it is 0.000000000001. The ABI never tells you decimals; the token contract does. The Ethereum units guide covers that conversion in detail.

The transaction decoder recognises a handful of common selectors (transfer, approve, transferFrom, safeTransferFrom, setApprovalForAll) on its own. For anything else, copy the raw input it prints and decode it against the contract's verified ABI. Do not trust a public selector database for anything you are about to sign: four bytes allow collisions, and people have deliberately mined signatures that share a selector with a well-known function.

From a hash to an explorer page

Most of the time you start with a hash, not raw bytes, and the right move is an explorer. The catch is that explorers disagree on URL paths and every EVM chain has its own.

ExplorerTransactionAddressBlock
Etherscan family/tx/0x.../address/0x.../block/123
Blockchair/ethereum/transaction/0x.../ethereum/address/0x.../ethereum/block/123
mempool.space (BTC)/tx/txid/address/addr/block/height-or-hash
Blockstream (BTC)/tx/txid/address/addr/block-height/123 or /block/hash

The Blockchain Explorer Link Generator builds these for Ethereum, Bitcoin, Polygon PoS, BNB Smart Chain, Arbitrum One, Base and OP Mainnet. It checks the identifier first. An EVM address in mixed case must pass the EIP-55 checksum: each letter is uppercase when the matching nibble of keccak256 of the lowercase address is 8 or more. Change one character and the checksum almost certainly fails, which is the cheapest typo check you will ever get. A lowercase address has no checksum, so the tool accepts it and returns the checksummed form.

If a hash shows "not found" on Etherscan, the transaction is most likely on another chain. Generate links for the same hash on Base, Arbitrum and Polygon and open them until one resolves. Testnet hashes need the testnet explorer.

Common mistakes

  • Pasting a hash into a raw decoder. The hash holds no fields. Fetch the raw bytes or use an explorer.
  • Computing a selector with uint or a space. Use uint256 and no spaces: transfer(address,uint256).
  • Decoding against a proxy's ABI. Upgradeable contracts forward calls to an implementation. Use the implementation ABI, or the decoder will report that no function has the selector.
  • Ignoring decimals. A decoded uint256 is base units. Divide by 10^decimals before reading it as an amount.
  • Assuming v is 27 or 28. On EIP-155 transactions it encodes the chain, and on typed transactions it is replaced by yParity.
  • Missing an unlimited approval. An approve amount of 2^256 - 1 (a 78-digit number starting 115792...) lets the spender move every token you hold, now and later.

When a transaction looks wrong, decode it in two passes: the envelope first, to check chain ID, nonce, recipient, value and fee caps, then the calldata against the real ABI. If both agree with what you meant to do, and the explorer link for the hash resolves on the chain you expected, you have checked everything the bytes can tell you. To price the fee caps you found, the Ethereum Gas Fee Calculator takes the same gwei values straight from the decoder's output.

Open the Ethereum Transaction Decoder