FORENSIAWeb3 forensicsOpen workspace

A timeline that groups by transaction and keeps its gaps

The activity timeline orders one result’s transfers and permission events by transaction and block. Where order or time is missing or contradictory, it says so and does not guess.

· Forensia · 7 min read · Timelines, Permissions

A list of transfers is hard to read as a story. Ten rows may belong to one transaction or to ten. A permission granted on Tuesday matters differently from one granted after the money moved. Investigators usually rebuild that sequence by hand.

Forensia now builds it for you from the records already in a result. Rows are grouped by the transaction they belong to, the groups are placed oldest block first, and permission events (grants and revocations of spending rights) sit in the same sequence as the transfers.

The timeline also keeps what it does not know. If the records disagree about which block a transaction is in, or the time is missing, or two transactions share a block and their order was not captured, the timeline shows that state in words. It never fills a gap from a neighboring entry.

#Plain languageA logbook with honest blank lines

Picture a ship’s logbook compiled from several loose notes. Good practice is to copy entries in order, staple together the notes that describe the same event, and leave a line blank where a note has no date. Bad practice is to write in the date that seems likely.

Each timeline entry is one transaction. It carries a block number (the position of the batch it was included in), a time when one is established, a short heading such as "Contract interaction" or "Failed transaction attempt", and a count of the activity records and source records behind it. If the entry includes permission events, it counts grants and revocations and reminds you that these are recorded events, not a current list of what is still allowed.

Two things the timeline is not. Block order is not a claim that one transaction caused another. And the steps inside a single transaction are grouped together, not laid out on a shared clock.

#How it worksReading the timeline on Overview

  1. Run a lookup for an address or a transaction and stay on Overview.
  2. Open "Read the activity timeline". The line beside it says how many recorded transactions it holds.
  3. Read down the list: oldest block first, unplaced records last. Times are shown in your local timezone. The first eight transactions are shown; a button reveals the remaining ones.
  4. Check the small labels under each block number. They tell you when a time is not established, when reported times disagree, and when a transaction shares a block with another and their order is not established.
  5. Use "Read transaction record" to jump to the exact row in Activity, or "Read permission source" for an entry that has only permission events. "Read block-position source" appears when the position inside the block was captured.
  6. Use "Back to timeline" to return. It reopens the same snapshot and makes no new lookup.

#Technical detailHow entries are built, placed and ordered

Grouping

The timeline is derived each time it is shown. It reads two arrays from the result: the activity rows and the permission events from the authorization analysis. Each record is keyed by its lowercased transaction hash, accepted only if it matches the expected shape (0x plus 64 hex characters on EVM networks, 64 hex characters on Bitcoin). Nothing is written back to the capture, and no source is contacted.

Block position

Every activity row and permission event in a group contributes its block number. A group gets a block only when every record has one and they are all the same non-negative integer. More than one distinct value, or an invalid value, is a position conflict and the entry reads "Conflicting block records". A missing value leaves "Block position unavailable".

There is one extra check for transaction lookups. The result’s subject carries the block that the queried transaction was included in. For the entry whose hash equals the query exactly, that block is compared with the rows. A disagreement withdraws the position. The subject block is never used to fill a missing row position, and for an address lookup it is ignored, because there it is only the height at which the lookup was made.

Time

Timestamps are parsed to instants, so two strings that express the same moment in different timezone offsets agree. An entry gets a time only when every activity row in the group has a valid timestamp and they all resolve to one instant. A missing timestamp leaves the time unestablished; an unparseable one, or two different instants, marks a time conflict. An entry made only of permission events has no activity time and none is invented.

A second pass compares across transactions. A block has one timestamp, so if two transactions in the same known block carry different valid instants, the derived time is withdrawn from every entry in that block. The block number stays.

The same pass also checks against the block header. When the lookup is for one transaction and its captured execution context passed its own source checks, the block time recorded in that context joins the comparison for that block. If it disagrees with an activity timestamp, the displayed time is withdrawn and the existing conflict warning is shown. The header time is a contradiction check only: it is never shown as the time in place of a missing activity timestamp. Wallet lookups, contexts that failed their checks and unrelated blocks contribute no header time, and the block position and transaction index stay available either way.

Position inside the block

A transaction index is shown only for the transaction you looked up, on an EVM network, and only when the result holds a valid execution context: three exact captured records from eth_getTransactionByHash, eth_getTransactionReceipt and eth_getBlockByNumber that agree with each other and with the row block. The label reads, for example, "Transaction 91 in captured block (index 90)". Index 0 is the first transaction. Other transactions in the same result do not inherit an index, and an address lookup supplies none.

Ordering and tie-breaks

SituationWhat decides the orderWhat the entry says
Different known blocksBlock number, ascendingBlock number
Same block, every entry has a unique index from one block hashTransaction index, ascendingTransaction position label
Same block, any index missing, duplicated or from a different block hashTransaction hash, alphabetical, as a display fallback onlySame block · transaction order not established
No block, or conflicting block recordsPlaced last, by transaction hashBlock position unavailable, or Conflicting block records

The all-or-nothing rule for a shared block is deliberate. Using an index for some comparisons and a hash for others would produce an order that looks precise and is partly made up. If two different transactions claim the same index, both lose it and are flagged as conflicting.

The index ordering was checked against a real pair in Ethereum block 23,176,991: 0x5b0837bfd469cb20f5474de25bb4270934133831fca64196e7845b33acdaead5 at index 90 and 0xa5623be48e0ee15e703f3b7d8eba4610afd086f2ba9cb3812b33a0aa271f4ea4 at index 89. The pair was chosen so that alphabetical hash order disagrees with position order, and the entry at index 89 came first.

Headings

A heading summarizes the group, not whichever row arrived first. It looks at the outer records only (native transfer, contract call or deployment rows that are not internal calls) and applies these rules in order:

  • If the outer records report both success and failure, or any is marked as having conflicting source records, the heading is "Conflicting transaction records".
  • "Failed transaction attempt" is used only when every outer record reports failure. Failed plus unknown is not enough.
  • A recognized protocol action supplies its own title, for the queried transaction only.
  • Mixed outer kinds fall back to the neutral "Captured transaction activity".
  • A failure in a nested call does not change the heading of the outer transaction.

Sources

The source count on an entry includes only source records that appear exactly once in the result with a captured status. A reference that is missing, duplicated or unavailable is not counted and cannot be opened from the timeline.

#LimitsWhat the sequence cannot tell you

  • Only records in this snapshot are included. An address lookup covers the part of history its sources returned, so an absent transaction is not evidence that nothing happened.
  • Order is not causation. An earlier block does not show that one transaction led to another.
  • Calls, transfers and events inside one transaction are not ordered against each other here.
  • A block position is agreement among captured provider records. It is not a consensus or finality check, and Forensia does not re-execute anything to confirm it.
  • A conflict label does not say which record is right, and it is not proof that a chain reorganization occurred.
  • Permission events are history. They do not show which approvals are still in force.
  • Within one result, at most one transaction can carry a captured index, so several transactions in one block will usually show as order not established.