Blockchain
Transactions and Receipts in Ethereum
When you click "Send" in MetaMask, a remarkable chain of events unfolds behind the scenes: your transaction is encoded in a format invented specifically for Ethereum, signed with a cryptographic signature, passes through the mempool, is executed by the virtual machine, generates a receipt with proof of the result, and is embedded into a Merkle tree. All of this takes 12 seconds. But what exactly happens inside those 12 seconds? How does a 1 ETH transfer differ from a Uniswap swap call? And how does your wallet find all your Transfer events among a trillion logs in milliseconds, without downloading the entire blockchain?
- **EIP-1559 and ETH burning** - since August 2021, every Type 2 transaction burns the baseFee, destroying ETH. In the first two years, more than 3.5 million ETH (~$6B) was burned. Understanding transaction structure explains why ETH can be a deflationary asset
- **Etherscan and indexers** - when you see transaction history on Etherscan, the service uses Bloom filters to quickly search for event logs across millions of blocks. The Graph indexes events for dApps: every Uniswap swap, every NFT mint is an event log discovered via Bloom
- **Layer 2 rollups (Optimism, Arbitrum)** - EIP-4844 blob transactions reduced the cost of publishing rollup data by 10–100×. Understanding transaction types explains why L2 transactions now cost fractions of a cent
Предварительные знания
Ethereum Transaction Types
A transaction in Ethereum is the **only way to change blockchain state**. No balance moves, no contract executes without a signed transaction. Since the network launched, the transaction format has evolved: from a single legacy format to four distinct types, each solving a specific problem.
Every transaction, regardless of type, contains a set of **required fields**. Let's look at the structure using the most common Type 2 (EIP-1559) as an example:
The **nonce** field is the account's transaction counter, starting from 0. Each new transaction must have a nonce exactly 1 greater than the previous one. This prevents replay attacks (re-submitting the same transaction) and guarantees strict ordering of transactions from a single account.
The key difference between Type 0 and Type 2 is the gas pricing model:
**Type 3 (blob transactions)** are fundamentally different: blob data is stored **outside the execution layer** and is automatically pruned after ~18 days. Rollups use them instead of calldata, reducing the cost of publishing data by 10–100×. Each blob is ~128 KB of data with its own gas market (blob gas), independent of the main one.
What is the main difference between an EIP-1559 (Type 2) and a legacy (Type 0) transaction in terms of fee distribution?
RLP: Ethereum Data Serialization
When a transaction is signed and ready to be broadcast to the network, it must be converted into a byte sequence. Ethereum uses **RLP (Recursive Length Prefix)** for this - its own serialization format, chosen for its **determinism** and **simplicity**. Unlike JSON or Protobuf, RLP guarantees that the same data always produces the same byte output.
**Determinism** is critical for a blockchain. If two nodes encode the same transaction differently (JSON, for example, allows different key ordering), their hashes won't match and consensus becomes impossible. RLP eliminates this problem: the format is so rigid that the canonical representation is unique.
RLP encodes only two primitives: **strings** (byte sequences) and **lists** (nested structures). All Ethereum data - numbers, addresses, hashes - is represented as byte strings. The encoding rules are:
Let's see how a simple ETH transfer is encoded in RLP:
For transaction types 1, 2, and 3, a **typed envelope** is used (EIP-2718): the encoded transaction starts with a type byte (`0x01`, `0x02`, `0x03`), followed by the RLP payload. Legacy transactions (Type 0) have no prefix - their RLP starts directly with `0xf8..` (the beginning of a long list).
Ethereum is gradually migrating from RLP to the more efficient **SSZ (Simple Serialize)** format used in the Beacon Chain (Proof of Stake). SSZ supports fixed and variable lengths, data types, and is optimized for Merkle proofs. However, the execution layer still uses RLP, and a full transition to SSZ is a task for future hard forks.
Why does Ethereum use RLP instead of widely used formats like JSON or Protobuf?
Event Logs and Bloom Filters
When a smart contract wants to notify the outside world about something that happened, it uses **event logs** - a special mechanism implemented through the `LOG0`–`LOG4` opcodes. Logs are not stored in the state trie (they are not accessible from other contracts), but they are recorded on the blockchain and indexed for fast lookup using **Bloom filters**.
In Solidity, event logs are declared with the `event` keyword, and parameters marked `indexed` go into topics:
Now the key question: how do you find all Transfer events among millions of blocks without downloading each one? This is where the **Bloom filter** helps - a probabilistic data structure embedded in the header of every block.
In practice, dApps use libraries like **ethers.js** to subscribe to events:
Event logs **cannot be read from a smart contract**. They are written via LOG opcodes, but the EVM has no opcode for reading logs. Logs are exclusively for external observers (dApps, indexers like The Graph, analytics services). If a contract needs the data - use storage.
The Bloom filter in an Ethereum block header shows that there is POSSIBLY a Transfer event from Alice in the block. What does this mean?
Transaction Receipts and Receipt Trie
After each transaction is executed, the EVM creates a **transaction receipt** - a record capturing the result. The receipt contains the status (success/revert), the actual gas used, all event logs, and the Bloom filter for that specific transaction. Receipts are not transmitted - they are **computed by each node** based on transaction execution.
All receipts in a block are combined into a **Receipt Trie** - a Merkle Patricia Trie whose root is recorded in the block header as `receiptsRoot`. Together with `transactionsRoot` and `stateRoot`, this creates a system of cryptographic proofs:
The Receipt Trie allows **light clients** (wallets, mobile apps) to verify a transaction result without downloading the entire blockchain:
**logsBloom in the block header** is the bitwise OR of all Bloom filters from individual receipts. This allows a single check (256 bytes) to determine whether a block might contain a desired event. If a bit is not set in the block's logsBloom - no transaction in the block contains that event, guaranteed.
A receipt **does not contain the return data** of a function (`returns (uint256)`). If you called `transfer()` and want to know the returned value - the receipt shows only the status (1 or 0) and event logs. Return data is only available at execution time (to the calling contract via RETURNDATACOPY) or by simulating the call via `eth_call`.
A transaction receipt is a confirmation sent by the network to the user after the transaction is included in a block
A receipt is a data structure that every node COMPUTES locally when executing the transaction. Nobody "sends" anything to anyone - any node that has executed the transaction will produce an identical receipt. Its correctness is guaranteed by its inclusion in the Receipt Trie, with the receiptsRoot in the block header.
This confusion transfers the Web2 mental model (a server sends a response to a client) onto the blockchain. In reality, a blockchain is a replicated state machine: all nodes execute the same transactions and independently arrive at the same result. A receipt is not a server response - it is a provable artifact of execution.
A light client wants to verify that a transaction in block #19000000 completed successfully. What data does it need?
Key Ideas
- **4 transaction types**: Legacy (Type 0) with fixed gasPrice, EIP-2930 (Type 1) with access lists, EIP-1559 (Type 2) with baseFee + priorityFee and burning, EIP-4844 (Type 3) with blob data for rollups
- **RLP - deterministic serialization** for Ethereum: encodes only strings and lists, guarantees a unique canonical representation. This is critical for matching hashes across all network nodes
- **Event logs (LOG0–LOG4)** - how contracts notify the outside world. Topics (up to 4) are indexed, data is not. Logs cannot be read from a contract - they are for dApps and indexers
- **Bloom filter (2048 bits)** in the block header allows eliminating a block from a search in a single check. False negatives are impossible, false positives ~0.1%. This gives wallets fast event search across millions of blocks
- In summary: from pressing "Send" to finalization, a transaction travels through RLP encoding → signing → execution → receipt → Receipt Trie → receiptsRoot in the block header - and it is this chain that allows a wallet to cryptographically prove the result in milliseconds without downloading the entire blockchain
Related Topics
Transactions are the central mechanism of Ethereum, connecting accounts, gas, the EVM, and scaling:
- Ethereum: Accounts and State — A transaction changes the sender's nonce, EOA/contract balances and storage - all data stored in the State Trie
- Gas: Economics of Computation — gasLimit, maxFeePerGas, priorityFee - transaction fields that determine execution cost. The receipt records actual gasUsed
- EVM: Virtual Machine — The EVM executes the transaction: bytecode from calldata, LOG0–LOG4 opcodes for event logs, SLOAD/SSTORE for state changes
- Rollups: Scaling — EIP-4844 blob transactions are the key mechanism for making L2 rollups cheaper when publishing data on L1
Вопросы для размышления
- Why did Ethereum decide to burn baseFee (EIP-1559) instead of giving all the fee to validators? What economic and security consequences does this create?
- A Bloom filter allows false positives. How would log search performance change if an exact index (without false positives) were used instead of Bloom? What trade-off did Ethereum make?
- A receipt doesn't contain the return data of a function. Why is this a design decision rather than a limitation? How do dApps work around it (hint: event logs and eth_call)?