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

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 byte | Type | Fields after the type byte |
|---|---|---|
| 0xc0 and up (often 0xf8) | Legacy | nonce, gasPrice, gasLimit, to, value, data, v, r, s |
| 0x01 | EIP-2930 | chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, yParity, r, s |
| 0x02 | EIP-1559 | chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, yParity, r, s |
| 0x03 | EIP-4844 blob | the EIP-1559 fields plus maxFeePerBlobGas and blobVersionedHashes |
| 0x04 | EIP-7702 | the EIP-1559 fields plus authorizationList |
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:
| Bytes | Meaning |
|---|---|
| f86c | List of 0x6c = 108 bytes |
| 09 | nonce 9 |
| 85 04a817c800 | gasPrice 20,000,000,000 wei = 20 gwei |
| 82 5208 | gasLimit 21000 |
| 94 3535...35 | to, a 20-byte address |
| 88 0de0b6b3a7640000 | value 10^18 wei = 1 ETH |
| 80 | data, empty |
| 25 | v = 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.
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.
| Explorer | Transaction | Address | Block |
|---|---|---|---|
| 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.