Skip to main content

The block array

A Data Feed delivers one row per block. Most onchain activity is exposed as typed arrays on that row (tokenSwaps, tokenTransfers, …), but the raw block header, transactions, logs, withdrawals, and access lists all arrive together in a single flattened array called block. If you’re coming from the REST API, where GET /{address}/logs hands you ready-made log objects, this is the biggest shape change to understand.

One array, many row types

Every element of block has the same wide shape: all the block*, transaction*, log*, and withdrawal* columns exist on every element. A discriminator column, itemType, tells you what each element actually is; only the columns matching that itemType are populated, the rest are null. itemType is one of: block, transaction, log, withdrawal, accessList. So for a block with 110 transactions, each emitting 4 logs, the block array contains roughly:
itemTypeCountPopulated columns
block1block* (number, hash, timestamp, miner, gas…)
transaction110transaction* (hash, from, to, value, receipt…)
log440log* (index, address, topic0-3, data)
551 elements in that one block row (plus any withdrawal / accessList rows).

⚠️ Log rows don’t carry the transaction hash

This is the detail that trips people up. A log element has its transactionIndex and logIndex, the emitting logAddress, the four topics, and the raw logData, but no transactionHash. To attach the hash, join each log to its sibling transaction element on transactionIndex:
  • itemType = 'transaction' AND transactionIndex = N → the tx hash + metadata
  • itemType = 'log' AND transactionIndex = N → that transaction’s logs
Block-level context (blockNumber, timestamp, blockHash) comes from the top-level row (position, timestamp, hash), not from the log element.

Worked example, two transactions, four logs each

Top-level row for the block: position = 21000000, hash = "0xblk…", timestamp = 1718800000. Below is an excerpt of its block array showing transactions #5 and #6 with their logs. Only populated columns are shown; everything else on each element is null.

Reading it

  • Get a transaction with its logs: filter itemType='transaction' for the hash/metadata and itemType='log' for the events, both on the same transactionIndex, then join on transactionIndex.
  • On-chain order of a log is (blockNumber, transactionIndex, logIndex), don’t rely on physical array position; use the index columns (or sequenceIndex for the raw interleaving).
  • Topics are flat columns. topic0 (the event signature hash) is always present; topic1..topic3 are null when the event has fewer topics. Re-assemble a topics[] array if your downstream expects one.
  • No decoding. logData + topics are raw hex, the event name and parameters are yours to decode from the contract ABI. (The REST API’s decoded/“verbose” responses do this for you; on a raw feed it’s a derive-yourself step.)
The logsByContract and logsByTopic0 recipes do exactly this filtering+join for you and write a flat one-row-per-log table, a drop-in for GET /{address}/logs.