When decoding raw Ethereum transactions, the first thing to confirm is not "how to run RLP decoding", but whether this byte string is pure RLP encoded data. Legacy transactions are plain RLP lists, while typed transactions introduced by EIP-2718 add one extra type byte before the RLP payload. If you directly feed a byte string starting with 0x02... into a standard RLP decoder, the first byte 0x02 will be treated as a "single-byte string", all following fields will be misaligned, and you will get a bunch of seemingly valid but actually meaningless garbage data.

A leading global cryptocurrency platform,suitable for both beginners and experienced traders.
New user benefit: 20% off trading fees upon registration!!
Check the first byte first: RLP list prefix, or type identifier
EIP-2718 specification is very clear: the format of typed transactions is TransactionType || TransactionPayload, where TransactionType is an unsigned 8-bit integer between 0 and 0x7f. Legacy transactions are defined as rlp([nonce, gasPrice, gasLimit, to, value, data, v, r, s]), the entire byte string is a single RLP list, no leading type byte.
There is only one judgment rule: Check the very first byte of the raw data.
If the first byte falls in the RLP list prefix range (from 0xc0 to 0xff), this byte string is a legacy transaction, directly run RLP decoding on the entire byte string, you will get a list containing 9 elements. If the first byte is 0x01, 0x02, 0x03 or any other value smaller than 0x80, it is a typed transaction: the first byte is the type identifier, the remaining bytes are the RLP-encoded payload, you need to strip the first byte first, then run RLP decoding on the rest.
OnFinality's guide describes this branch logic as "the difference between a correct decoder and a decoder that silently generates garbage fields". This is no exaggeration. If you misprocess an EIP-1559 transaction starting with 0x02 as legacy RLP data, the decoder will read 0x02 as a single-byte string, then parse the following 0xf8 or 0xf9 as the list prefix, the field order and meaning will be completely mismatched.
Common transaction types corresponding to type bytes
On Ethereum mainnet, the type bytes you will most often encounter are the following:
0x01 corresponds to EIP-2930 access list transaction, the RLP list structure of its payload is [chainId, nonce, gasPrice, gasLimit, to, value, data, accessList, yParity, r, s]. 0x02 corresponds to EIP-1559 dynamic fee transaction, the structure is [chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, yParity, r, s]. 0x03 corresponds to EIP-4844 blob transaction, its structure adds blob-related fields based on EIP-1559. 0x04 corresponds to EIP-7702 authorization list transaction, its payload includes authorizationList.
Legacy transactions have no type byte, so its "type" is usually represented by 0 or null in code. BloXroute's documentation marks legacy transactions as "Type 0 legacy transaction", while typed transactions explicitly carry fields like type: '0x2'.
After stripping the type byte: RLP decoding itself is very simple
RLP encoding rules are not complicated, the core logic is that the prefix byte determines the meaning and length of the following bytes. For single-byte values from 0x00 to 0x7f, the byte itself is the encoded value; short strings (0–55 bytes) start with 0x80 + length; long strings start with 0xb7 + number of length bytes; short lists start with 0xc0 + payload length; long lists start with 0xf7 + number of length bytes.
Ethereum official documentation specifically emphasizes one rule: Positive integers must be represented in big-endian format with no leading zeros, any positive integer with leading zeros encountered during deserialization should be considered invalid. This rule is useful when you manually decode or write verification logic, but if you use mature libraries (viem's parseTransaction, ethers' parse, Alloy's TxEnvelope), the library will handle these internally.
One edge case to note: RLP encodes the structure, not the data type. The decoder returns nested byte arrays, each leaf node is a segment of hexadecimal data. You need to interpret the bytes as addresses, integers or byte arrays according to the field order of the corresponding transaction type. For example, the decoded to field is a byte segment, you need to check if it is a 20-byte address; the decoded value is bytes, you need to convert it to an integer in big-endian order. Libraries handle RLP-level decoding for you, but the semantic mapping of fields still relies on your understanding of the transaction type.
If you don't want to write your own decoder
viem's parseTransaction accepts a hexadecimal string and returns the parsed transaction object, its documentation states that it supports signed and unsigned EIP-1559, EIP-2930 and Legacy transactions. ethers's @ethersproject/transactions submodule provides the parse function to do the same thing. Alloy uses the TxEnvelope type to carry the decoding result, and distinguishes between cases with and without leading type bytes.
If you use these libraries, the type identification step is already handled for you. But understanding the first byte judgment logic is still valuable: when the fields returned by the library do not match your expectations, you can quickly confirm whether "the byte string is not the transaction type you thought it was", or "the library version does not support this type". For example, an EIP-7702 transaction starting with 0x04 may fail to parse or return wrong fields in an old library version that only supports up to EIP-1559.
Verify if you decoded correctly
The most direct verification is keccak256(raw bytes) == transaction hash. This equation holds for both legacy and typed transactions, the type byte is included in the pre-image of the hash. If you get the raw bytes from the RPC's eth_getRawTransactionByHash method, run keccak256 on it and compare it with the requested transaction hash. If they do not match, it means the endpoint returned wrong bytes (possible causes are proxy cache, wrong chain, or expired data).
The second layer of verification is to cross-check against the return object of eth_getTransactionByHash. Fields like type, nonce, to obtained by decoding the raw bytes should be consistent with the JSON object. If the raw bytes decode to type: 2 but the JSON object says type: 0, at least one of the two is wrong, and usually the wrong part is the raw bytes you got or the endpoint you are using.
A common pitfall: not all "raw transactions" follow mainnet format
If you process transactions on L2 or OP Stack chains, the range of type bytes may be wider. The OP Stack version of viem supports deposit transactions starting with 0x7e. Mantle's documentation also mentions the same deposit type. These types do not exist in the mainnet specification, but are legal on the corresponding chain. If your decoder hardcodes "only accept 0x01/0x02/0x03", it will fail when encountering 0x7e. The judgment rule remains the same: if the first byte is less than 0x80, treat it as a type byte and strip it, but you need to decide which type values to support according to the specification of the target chain.
Where you should start
If you have the raw bytes of a specific transaction at hand, first use eth_getRawTransactionByHash to confirm that what you get is indeed raw bytes, not a decoded object. Check the first byte: values above 0xc0 follow the legacy RLP decoding path, values below 0x80 follow the typed transaction path. Use viem or ethers' parsing function for a cross-verification, then cross-check the hash with keccak256. After completing these three steps, you can at least confirm "what format the bytes I got are in, and whether the decoded fields are self-consistent". As for the specific meaning of the fields and business logic, that is what you need to care about after the decoding is confirmed correct.

A leading global cryptocurrency platform,suitable for both beginners and experienced traders.
New user benefit: 20% off trading fees upon registration!!


