BR Academy
0%
Blockchain Data Fundamentals·Lesson 1 of 23·8 min

Blocks, transactions, and the mempool

Before you write a single SQL query, you need a mental model of what you're actually querying. Every blockchain analytics table — Dune's included — is ultimately a flattened, queryable view of three physical layers: blocks, transactions, and the mempool they came from. Get this layer wrong and every downstream query, no matter how elegant the SQL, will answer the wrong question.

A block is a batch of transactions bundled together with a header: a block number, a timestamp, a parent hash (linking it to the previous block), a state root (a cryptographic summary of account balances after the block executes), and a gas-used total. Think of a block as a single committed "save point" for the entire chain's state. In Dune, this header information lives in tables like ethereum.blocks — one row per block, with columns for number, time, gas_used, gas_limit, and miner (or validator post-Merge).

A transaction is a single signed instruction from one externally-owned account (a wallet) to either another wallet (a plain transfer) or a smart contract (a function call). Every transaction has a hash, a from address, an optional to address, a value (ETH moved directly), gas_price/gas_used, and a nonce — a strictly increasing counter per sender that prevents replay and determines transaction ordering for that account. Transactions live in ethereum.transactions, one row per transaction, foreign-keyed to their block by block_number.

Since the London upgrade, transaction fees split into two components you'll see as separate columns: base_fee_per_gas, a protocol-set value that burns (is destroyed, not paid to anyone) and adjusts block-by-block based on how full the previous block was, and priority_fee (the "tip"), which goes to whoever produced the block. This matters for analytics because gas_used * (base_fee + priority_fee) is the actual cost a sender paid — a figure you'll need whenever you build cost-of-activity dashboards, and a figure that is easy to get wrong if you only look at gas_price on a legacy-style transaction without accounting for the burn.

Before a transaction is included in a block, it sits in the mempool — a holding area of pending, unconfirmed transactions broadcast across the network. Miners (pre-Merge) or validators (post-Merge, under proposer-builder separation) select transactions from the mempool and order them into the next block, generally prioritizing higher gas_price / priority_fee bids, though MEV (maximal extractable value) searchers and builders can reorder for profit. The mempool itself isn't queryable historically in Dune — once a transaction lands in a block or is dropped, the pending state is gone — but understanding it explains why two transactions submitted seconds apart can land in different blocks, or why a transaction can sit pending for minutes during network congestion.

One subtlety worth internalizing early: blocks are not always permanent the instant they're produced. A re-org happens when two miners/validators produce competing blocks at the same height and the network later converges on one chain, discarding the other. The discarded block's transactions return to the mempool (or are dropped). An uncle block (pre-Merge terminology) is a validly-mined block that lost this race — its transactions didn't make it into the canonical chain. Dune's tables reflect only the canonical, finalized chain, so by the time data lands in ethereum.blocks, re-orgs have already been resolved — but this is why some real-time dashboards wait for several confirmations before counting a transaction as final.

Worked example: Open Etherscan and look up any recent Ethereum block, say block 21,000,000. Note its timestamp, its gas used relative to gas limit (this ratio tells you how congested the network was), its transaction count, and its miner/validator. Now imagine the matching row in ethereum.blocks: number = 21000000, time matching the Etherscan timestamp (Dune stores this in UTC), gas_used matching the "Gas Used" figure, and a tx_count derived by counting rows in ethereum.transactions where block_number = 21000000. Pick any individual transaction inside that block on Etherscan — note its from, to, value, gas price, base fee, priority fee, and nonce. These map directly, field-for-field, onto a row in ethereum.transactions. The exercise here is deliberately manual: you're building intuition for "a block is a container, a transaction is one line inside it" before you ever touch SQL.

Exercise: Pick any recent Ethereum block number from a block explorer. Without writing SQL, write down the five fields you'd expect to find in Dune's transactions table for one transaction in that block (for example: hash, from, to, value, gas_used). Then write down one field you suspect is missing from the raw transactions table — and why. (Hint: think about what a transaction calls versus what it does — the answer previews why Module 1's later lessons on logs and traces exist at all.) Finally, estimate the total fee paid for that transaction using base fee, priority fee, and gas used, and compare it against the "Transaction Fee" figure shown on the explorer.

Recap:

  • A block is a timestamped container of transactions plus a state-root "save point"; a transaction is one signed instruction with a hash, sender, optional recipient, value, nonce, and a base-fee/priority-fee cost split.
  • The mempool holds pending transactions before inclusion; re-orgs and uncle blocks mean "recent" history can briefly shift, though Dune's tables reflect only the finalized canonical chain.
  • Every later SQL lesson assumes this layer: a transaction row tells you that something happened and who paid for it, but not necessarily what happened inside — that's logs and traces, next.