Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides14 min read

Ethereum Block Retrieval: Full Transactions, Hashes, and Uncles

A technical deep-dive into eth_getBlockByNumber, eth_getBlockByHash, transaction count methods, and the deprecated uncle surface — with runnable Node.js examples and a reproducible measurement method.

TL;DR

Ethereum JSON-RPC exposes two primary block retrieval methods — eth_getBlockByNumber and eth_getBlockByHash — each accepting a boolean fullTx parameter that selects between an array of full transaction objects and an array of 32-byte transaction hashes. The compact hashes form is dramatically smaller and is the correct default for indexers that fetch transactions separately by hash, while the full form is useful for one-shot block processing. Transaction count methods (eth_getBlockTransactionCountByNumber and eth_getBlockTransactionCountByHash) return only the count, enabling cheap coverage checks without transferring the block body. The legacy uncle/ommer methods (eth_getUncleByBlockNumberAndIndex, eth_getUncleCountByBlockNumber, eth_getUncleByBlockHashAndIndex, eth_getUncleCountByBlockHash) return null or 0x0 for post-Merge blocks because Ethereum blocks no longer carry uncles, but parsers must still handle the fields for pre-Merge historical blocks and some non-Ethereum EVM chains. This article provides runnable Node.js examples, a reproducible measurement method, and a troubleshooting playbook for block retrieval over RPC.

Block Retrieval Methods and the fullTx Boolean

The Ethereum JSON-RPC specification defines two primary methods for retrieving a block: eth_getBlockByNumber(blockParameter, fullTx) and eth_getBlockByHash(blockHash, fullTx). Both accept a boolean fullTx parameter that controls the representation of the block's transaction list. When fullTx is true, the response includes an array of full transaction objects; when false, it includes an array of 32-byte transaction hashes. This boolean is the single most important parameter for controlling response size and downstream processing logic.

The hashes form is dramatically smaller because each transaction hash is exactly 32 bytes, while a full transaction object includes fields such as from, to, value, gas, gasPrice, input, v, r, s, and potentially accessList or maxFeePerGas. For a block with hundreds of transactions, the difference can be orders of magnitude in payload size. Indexers that will fetch transactions separately by hash should default to fullTx=false to avoid transferring the same data twice.

The two forms are consistent: each hash in the compact form corresponds to a full object that eth_getTransactionByHash returns with the same fields. However, during a reorg, the block identified by a given hash may be replaced, and the transaction list can drift. Pinning a numeric block number makes reads reproducible because the block number is a stable coordinate, whereas 'latest' is a moving target.

  • eth_getBlockByNumber(blockParameter, fullTx) — blockParameter can be a hex block number, or a named tag: 'latest', 'pending', 'safe', 'finalized'.
  • eth_getBlockByHash(blockHash, fullTx) — blockHash is a 32-byte hash; fullTx controls transaction representation.
  • fullTx=true returns an array of full transaction objects; fullTx=false returns an array of 32-byte transaction hashes.
  • The hashes form is the correct default for indexers that fetch transactions separately by hash.

Named Block Tags: latest, pending, safe, and finalized

The blockParameter argument accepts named tags in addition to numeric block numbers. 'latest' refers to the most recent block known to the node, 'pending' refers to a block that is not yet finalized (and may not exist as a canonical block), 'safe' refers to a block that is justified under the consensus rules, and 'finalized' refers to a block that is irreversible under the consensus rules. Under proof-of-stake, 'safe' and 'finalized' have specific meanings defined by the consensus layer: 'safe' is the latest justified checkpoint, while 'finalized' is the latest finalized checkpoint.

The difference between 'safe' and 'finalized' matters for applications that require different levels of assurance. A 'safe' block is very unlikely to be reorged but is not guaranteed irreversible; a 'finalized' block is considered irreversible under the protocol's finality mechanism. For reproducible reads, pinning a numeric block number is preferable because it eliminates ambiguity about which block the node considers 'latest' at the time of the request.

Not every client supports the 'safe' and 'finalized' tags. This behavior is documented / varies by client. If a client does not support these tags, it may return an error or fall back to 'latest'. Always check the client's documentation and test the tag against your endpoint before relying on it in production.

  • 'latest' — most recent block known to the node; can change between requests.
  • 'pending' — a block that is not yet finalized; may not exist as a canonical block.
  • 'safe' — latest justified checkpoint under PoS; very unlikely to be reorged but not irreversible.
  • 'finalized' — latest finalized checkpoint under PoS; considered irreversible.
  • Pinning a numeric block number makes reads reproducible and eliminates ambiguity.

Transaction Count Methods for Cheap Coverage Checks

The methods eth_getBlockTransactionCountByNumber and eth_getBlockTransactionCountByHash return only the number of transactions in a block, without transferring the block body. This is useful for cheap coverage checks: an indexer can verify that it has processed the expected number of transactions for a block without downloading the full transaction list. The count is returned as a hexadecimal string, consistent with other JSON-RPC numeric values.

These methods are particularly valuable when combined with the hashes form of block retrieval. A pipeline can fetch the block with fullTx=false to get the transaction hashes, then use eth_getBlockTransactionCountByNumber to confirm the count matches the length of the hash array. If the counts diverge, the block may have been reorged or the provider may have truncated the response.

For bulk receipt retrieval, eth_getBlockReceipts returns all receipts for a block in a single call, which is more efficient than fetching receipts one by one. The eth_getBlockReceipts bulk receipts article covers that method in detail.

  • eth_getBlockTransactionCountByNumber(blockParameter) — returns the transaction count for a block identified by number or tag.
  • eth_getBlockTransactionCountByHash(blockHash) — returns the transaction count for a block identified by hash.
  • Use these methods to verify that the length of a transaction hash array matches the expected count.
  • Combine with eth_getBlockReceipts for efficient bulk receipt retrieval.

Legacy Uncle Methods and Post-Merge Behavior

The legacy uncle/ommer methods are eth_getUncleByBlockNumberAndIndex, eth_getUncleCountByBlockNumber, eth_getUncleByBlockHashAndIndex, and eth_getUncleCountByBlockHash. These methods were designed for proof-of-work Ethereum, where blocks could reference uncle blocks (also called ommers) that were valid but not part of the canonical chain. Since the Merge in September 2022, Ethereum blocks no longer carry uncles, so these methods return null or 0x0 for post-Merge blocks.

However, a parser must still handle the fields because pre-Merge historical blocks and some non-Ethereum EVM chains genuinely have uncles. The Block object includes an 'uncles' array and a 'sha3Uncles' field. The 'uncles' array contains the hashes of uncle blocks, and 'sha3Uncles' is the Keccak-256 hash of the uncle list. An empty 'uncles' array does not by itself prove a block is post-Merge; to be sure, pin to a block number above the Merge block. The Ethereum execution-apis schema documents the post-Merge removal of uncles.

The uncle methods are deprecated and should be treated as read-only legacy surface, never as a source of new consensus data. For pre-Merge blocks, they remain useful for historical analysis. For post-Merge blocks, they are effectively no-ops. The Ethereum JSON-RPC specification documents these methods and their parameters.

  • eth_getUncleByBlockNumberAndIndex(blockParameter, index) — returns the uncle block at the given index, or null if none.
  • eth_getUncleCountByBlockNumber(blockParameter) — returns the number of uncles in a block, or 0x0 if none.
  • eth_getUncleByBlockHashAndIndex(blockHash, index) — returns the uncle block at the given index for a block identified by hash.
  • eth_getUncleCountByBlockHash(blockHash) — returns the number of uncles for a block identified by hash.
  • Post-Merge blocks return null or 0x0; pre-Merge blocks and some non-Ethereum EVM chains may have uncles.

Runnable Node.js Example: Comparing Full and Hash Representations

The following Node.js example fetches the same block in both representations, asserts that the hash array length equals the full array length, and verifies that each hash matches the corresponding full transaction's hash. It uses the built-in fetch API available in Node.js 18 and later. Replace the RPC_URL with your endpoint, such as an Ethereum RPC node from OnFinality.

This example demonstrates the consistency between the two forms and provides a foundation for building indexers that use the compact form for storage and the full form for processing. The assertion logic can be extended to check other fields, such as block number and parent hash, to detect reorgs.

const RPC_URL = 'https://your-ethereum-rpc-endpoint';

async function rpcCall(method, params) {
  const response = await fetch(RPC_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
  });
  const data = await response.json();
  if (data.error) throw new Error(data.error.message);
  return data.result;
}

async function compareBlockRepresentations(blockNumberHex) {
  const [blockWithHashes, blockWithFullTx] = await Promise.all([
    rpcCall('eth_getBlockByNumber', [blockNumberHex, false]),
    rpcCall('eth_getBlockByNumber', [blockNumberHex, true])
  ]);

  const hashes = blockWithHashes.transactions;
  const fullTxs = blockWithFullTx.transactions;

  console.log('Block number:', blockWithHashes.number);
  console.log('Hash array length:', hashes.length);
  console.log('Full tx array length:', fullTxs.length);

  if (hashes.length !== fullTxs.length) {
    throw new Error('Length mismatch: ' + hashes.length + ' vs ' + fullTxs.length);
  }

  for (let i = 0; i < hashes.length; i++) {
    if (hashes[i] !== fullTxs[i].hash) {
      throw new Error('Hash mismatch at index ' + i + ': ' + hashes[i] + ' vs ' + fullTxs[i].hash);
    }
  }

  console.log('All hashes match corresponding full transaction hashes.');
  return { hashes, fullTxs };
}

// Example: fetch a specific block by number
compareBlockRepresentations('0x112A880').catch(console.error);

Reproducible Method: Comparing Block Sizes Across Representations

To measure the size difference between the full and hash representations, you can fetch the same block with fullTx=true and fullTx=false, serialize each response to JSON, and compare the byte lengths. This method is reproducible across endpoints and block numbers. The results will vary by block, so it is useful to sample multiple blocks and record the results in a table.

The following Node.js snippet fetches a block in both representations, measures the JSON string length, and prints the ratio. You can run this against your own endpoint and fill in the results table below. The measurement is deterministic for a given block and endpoint, but the absolute sizes depend on the number of transactions and the size of each transaction's input data.

Use a numeric block number rather than 'latest' to ensure reproducibility. If you are comparing across providers, use the same block number and the same fullTx settings. Note that some providers may cap or truncate extremely large blocks, which can affect the measurement.

async function measureBlockSize(blockNumberHex) {
  const [blockWithHashes, blockWithFullTx] = await Promise.all([
    rpcCall('eth_getBlockByNumber', [blockNumberHex, false]),
    rpcCall('eth_getBlockByNumber', [blockNumberHex, true])
  ]);

  const hashesJson = JSON.stringify(blockWithHashes);
  const fullTxJson = JSON.stringify(blockWithFullTx);

  const hashesBytes = Buffer.byteLength(hashesJson, 'utf8');
  const fullTxBytes = Buffer.byteLength(fullTxJson, 'utf8');

  console.log('Block:', blockNumberHex);
  console.log('Hashes form bytes:', hashesBytes);
  console.log('Full tx form bytes:', fullTxBytes);
  console.log('Ratio (full/hashes):', (fullTxBytes / hashesBytes).toFixed(2));

  return { blockNumberHex, hashesBytes, fullTxBytes, ratio: fullTxBytes / hashesBytes };
}

// Example: measure a block
measureBlockSize('0x112A880').catch(console.error);

Results Table for Reader Measurements

Use the table below to record your own measurements. Run the measurement snippet against your endpoint for several block numbers, including a mix of low-activity and high-activity blocks. The table columns capture the block number, the number of transactions, the byte size of the hashes form, the byte size of the full transaction form, and the ratio. This will help you understand the storage and bandwidth tradeoffs for your specific use case.

Because the results depend on the block and the provider, there is no single correct value. The goal is to establish a baseline for your own infrastructure. If you are using OnFinality's API service, you can run these measurements against your dedicated endpoint. For pricing considerations, see RPC pricing.

  • Block number | Transaction count | Hashes form bytes | Full tx form bytes | Ratio (full/hashes)
  • 0x112A880 | (fill) | (fill) | (fill) | (fill)
  • 0x112A881 | (fill) | (fill) | (fill) | (fill)
  • 0x112A882 | (fill) | (fill) | (fill) | (fill)
  • Add rows for additional blocks as needed.

Troubleshooting Block Retrieval Over RPC

When block retrieval fails or returns unexpected results, the cause is often one of a few common issues. First, check that the block parameter is correctly formatted: numeric block numbers must be hex-encoded strings (e.g., '0x112A880'), and named tags must be lowercase. Second, verify that the fullTx boolean is a boolean, not a string. Third, confirm that the block exists on the chain your endpoint is serving; a block number above the chain tip will return null.

If you receive a null result for a block that should exist, the node may be behind the chain tip. The Detecting an RPC node behind the chain tip article covers methods for diagnosing lag. If you are using 'safe' or 'finalized' tags and receive an error, the client may not support those tags; fall back to 'latest' or a numeric block number. If the transaction count from eth_getBlockTransactionCountByNumber does not match the length of the transaction array, the block may have been reorged or the provider may have truncated the response.

For reorg-related issues, the Ethereum block reorg detection and RPC depth article provides a deeper treatment. For indexer reconciliation, see Block-by-block EVM indexer reconciliation.

  • Ensure block parameters are hex-encoded strings or valid named tags.
  • Verify fullTx is a boolean, not a string.
  • Check that the block exists on the chain served by your endpoint.
  • If 'safe' or 'finalized' fails, the client may not support those tags.
  • Mismatched transaction counts may indicate a reorg or provider truncation.

Limitations and Tradeoffs

Several limitations apply to block retrieval over JSON-RPC. Some providers cap or truncate extremely large blocks, which can result in incomplete transaction lists. The 'safe' and 'finalized' tags are not supported by every client; this behavior is documented / varies by client. Uncle methods are deprecated and should be treated as read-only legacy surface, never as a source of new consensus data.

The full transaction representation is convenient but can be expensive in terms of bandwidth and storage. The hashes representation is compact but requires additional calls to fetch transaction details. The choice depends on your use case: if you need to process every transaction in a block immediately, the full form may be simpler; if you are building an indexer that stores transactions separately, the hashes form is more efficient.

For a broader overview of Ethereum RPC node setup and usage, see the Ethereum RPC node guide (RPC Assistant). For more articles on network and protocol topics, visit the OnFinality Learn hub.

  • Provider caps or truncation may affect extremely large blocks.
  • 'safe' and 'finalized' tags are not universally supported.
  • Uncle methods are deprecated and return null or 0x0 post-Merge.
  • Full transaction form increases bandwidth and storage costs.
  • Hashes form requires additional calls to fetch transaction details.

Next Steps for Building Robust Block Retrieval Pipelines

To build a robust block retrieval pipeline, start by pinning numeric block numbers for reproducible reads. Use the hashes form as the default for indexers, and fetch full transactions only when needed. Implement consistency checks by comparing transaction counts from eth_getBlockTransactionCountByNumber with the length of the transaction array. Handle reorgs by monitoring parent hashes and using a confirmation depth appropriate for your application.

For high-throughput indexing, consider using eth_getBlockReceipts to fetch receipts in bulk, and combine it with the hashes form to minimize payload size. If you are running your own node, ensure it is not behind the chain tip by monitoring block numbers. For managed infrastructure, OnFinality's Ethereum RPC node and API service provide endpoints that support these methods.

Finally, keep the deprecated uncle methods in your parser for historical compatibility, but do not rely on them for new consensus data. The Ethereum execution-apis repository documents the schema and the post-Merge removal of uncles. For pricing and plan details, see RPC pricing.

  • Pin numeric block numbers for reproducible reads.
  • Default to fullTx=false for indexers; fetch full transactions only when needed.
  • Use eth_getBlockTransactionCountByNumber for cheap coverage checks.
  • Monitor parent hashes and confirmation depth to handle reorgs.
  • Keep uncle methods for historical compatibility but do not rely on them for new data.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started