An Ethereum transaction hash identifies a transaction, while logIndex identifies a log's position within one block. One transaction can emit several logs, and a reorganization can move its execution into a different block with different log positions. Identify each observed log occurrence with its network, block hash and log index, then track whether that occurrence belongs to the current canonical chain.
The identifier tells you which question to ask
A transaction is the container for an execution; an event log is one output of that execution. Looking up a transaction hash is a useful starting point, but treating that hash as an event ID collapses every log in its receipt into one record.
Ethereum's JSON-RPC documentation gives logs separate transaction, block and position fields, along with an emitting address, topics and data. In particular, logIndex is the position in the entire block, not the position within the originating transaction. Ethereum JSON-RPC log fields.
| Field or context | Question it answers | Boundary to retain |
|---|---|---|
| Network identity | Which chain are we observing? | Keep a configured network namespace with the records. |
| transactionHash | Which transaction produced this log? | Several logs can share it. |
| blockHash | Which particular block contains this occurrence? | A height can have a different canonical hash after a reorganization. |
| transactionIndex | Where is the transaction inside that block? | It does not identify an individual log. |
| logIndex | Where is this log inside that block? | It needs a block and network namespace. |
| address, topics, data | Which emitter and encoded event are represented? | Decoding meaning requires the relevant contract schema. |
Use a stable local network name tied to the expected chain configuration; record the chain ID as supporting context. Avoid letting two unrelated environments share a database namespace merely because someone configured the same numeric chain ID.
This is a different problem from exchange trade IDs versus order IDs. An exchange execution belongs to the venue's record system. An Ethereum log occurrence belongs to a particular block execution, whose place in accepted history can change.
One transaction, two block placements
Consider a hypothetical transaction T on one Ethereum network. It emits two transfer logs from a tracked token contract to a tracked recipient: 7 units and 5 units. Assume those are the only relevant logs, the token's decoding is already verified, and the same two amounts are emitted when T is included again. These are invented quantities and placements, not observations from an explorer.
At height 25,000,100, block A initially contains T. Other transactions produced 40 logs before T, so its two logs have indices 40 and 41. A reorganization replaces A with block B at the same height. In B, earlier transactions produced 13 logs, so T's logs have indices 13 and 14.
| Placement | Transaction | Block hash alias | logIndex | Tracked units |
|---|---|---|---|---|
| Initial branch | T | A | 40 | 7 |
| Initial branch | T | A | 41 | 5 |
| Replacement branch | T | B | 13 | 7 |
| Replacement branch | T | B | 14 | 5 |
A, B and T are short teaching aliases, not valid hashes. Actual records should preserve the complete identifiers. The table has four distinct observed occurrences, but only two belong in the final canonical total after B replaces A.
Geth documents the relevant subscription behavior: logs previously sent from the old chain are sent again with removed set to true; logs from transactions on the new chain are emitted. Consequently, a subscriber can receive logs for the same transaction more than once. This is Geth's documented behavior, not a delivery guarantee for every hosted service. Geth real-time log subscriptions.
Now add ordinary repeated delivery to the hypothetical sequence. Each table row below represents a pair of log observations, one for each amount. The local ledger starts at zero and records only this example's contribution.
| Observation | Required local action | Canonical units after action |
|---|---|---|
| A:40 and A:41 added | Record and activate both occurrences | 12 |
| Same additions delivered again | Retain observations; apply no extra quantity | 12 |
| A:40 and A:41 marked removed | Deactivate A's occurrences and reverse their contribution | 0 |
| B:13 and B:14 added | Record and activate B's occurrences | 12 |
| B's pair found again during backfill | Match existing active occurrences | 12 |
The correct final quantity is 7 + 5 = 12 units. There are ten log observations: eight additions and two removals. Summing all positive observations gives 4 × 12 = 48 units. Deduplicating occurrences but never reversing A gives 12 + 12 = 24 units. Both totals are wrong for the final canonical view.
The observed index shift is 13 − 40 = −27 for the first log and 14 − 41 = −27 for the second. The unchanged transaction hash does not make the old indices permanent. This example's unchanged amounts are also an assumption. Ethereum's state-transition input includes the starting state as well as transactions; the indexing implication is that a matching transaction hash alone cannot establish matching output after a new inclusion. Inspect the new receipt. Ethereum state-transition model.
Keep observed occurrences and the current view separately
A practical local occurrence key for an included log is network namespace plus blockHash plus logIndex. Store transactionHash as a relationship and lookup field. This is an indexing design recommendation derived from the field scopes, not an Ethereum requirement for a database schema.
Require a non-null blockHash and logIndex before creating that occurrence key. The JSON-RPC log schema permits these placement fields to be null for pending logs. Keep such observations separately until inclusion; do not convert missing positions to zero. An actual logIndex of zero is a valid block position.
Keep the raw emitter, topics and data alongside that key. Store block number, transaction position, retrieval source and observation time as evidence. Separate the log's payload from its current canonical status and from the notification's removed flag. An addition and a removal can describe the same occurrence with opposite implications for the current view.
That separation supports three different operations:
- A repeated addition with matching payload records another observation without adding units again.
- A confirmed removal changes the existing occurrence's status and reverses any contribution currently applied from it.
- A new canonical inclusion creates different occurrence records, even if transactionHash matches an older inclusion.
Make the status transition and its local accounting effect atomic where possible. A crash after adding units but before recording progress can otherwise cause a second addition on restart. Persist a checkpoint together with the records it claims to cover, or use a recovery procedure that can safely replay them.
Do not repair identity by using transactionHash alone or by renumbering each receipt's logs from zero and assuming those positions identify permanent economic events. Such receipt-local positions may help inspect one execution, but the canonical execution must still be reconciled. The receipt status versus token transfer guide explains the separate check that a decoded log supports the intended asset movement.
If a matching occurrence key arrives with a different address, topics or data, keep the conflicting evidence and investigate the endpoint and parser. Changing the key until both versions fit would conceal the discrepancy. Conversely, a changed removed flag is an expected lifecycle observation, not automatically a payload conflict.
External actions need their own treatment. Reversing a local displayed total cannot undo an email, exchange order or withdrawal already initiated from an early log. Define when those actions become eligible and how duplicates or later reversals are handled. A unique database key alone does not make external actions occur exactly once.
Recover the branch after a missed removal
A live connection is an observation channel, not a complete historical ledger. Geth says subscriptions cover current events and disappear when the underlying connection closes. Reopening a socket does not establish what happened during the gap. Geth subscription considerations.
EIP-234 describes the specific failure: a connection can drop before a reorganization, leaving the client unaware that earlier logs were removed. It also explains why querying a height can return logs from a different block, while an empty result provides no returned block hash to inspect. EIP-234 recovery rationale.
Use the following recovery procedure as a design worksheet:
- Read the stored checkpoint's height and hash, then compare that hash with the current canonical block at that height.
- If they differ, walk stored and retrieved ancestry back to a common ancestor. If retained history is insufficient, stop claiming complete coverage and obtain a wider backfill.
- Deactivate applied occurrences from displaced blocks. Preserve their observations for explanation and audit.
- Read the replacement branch's blocks and retrieve the relevant logs for each particular hash where the endpoint supports it.
- Re-read the canonical hash at the intended checkpoint height before committing progress. If it differs from the branch just retrieved, repeat reconciliation. Apply matching canonical occurrences once, then advance the checkpoint only after the checked branch and local records agree.
The blockHash filter introduced by EIP-234 targets a particular block rather than a height range; it is an alternative to fromBlock and toBlock. Validate support and retained history at the actual endpoint. A hash-specific answer identifies the queried block's results; it does not establish that this block remains canonical. EIP-234 specification.
A late addition for A must not reactivate A while its block is still off the canonical branch. If competing observations arrive out of order, reconcile block membership before changing the derived total. If A later becomes canonical again, recompute the affected view from the newly established branch rather than treating “removed once” as a permanent prohibition.
Keep chain settlement policy separate from occurrence identity. The confirmations versus finality guide covers that decision. A preview from eth_call or a gas estimate also cannot substitute for the receipt and block evidence of an actual inclusion.
A small replay test exposes the wrong key
Before trusting a dashboard or indexer, run the invented sequence through its record model. Record the expected result before executing the test. The first three rows below form one sequence; the remaining rows are separate fault injections.
| Test variation | Expected result |
|---|---|
| Deliver A's additions three times | Two active occurrences; 12 units |
| Remove A twice | Zero active occurrences; zero units, with no second subtraction |
| Add B and replay its backfill | Two active occurrences; 12 units |
| Miss removals while disconnected | Branch reconciliation removes A before the checkpoint claims B's history |
| Deliver an old A addition after recovery | A remains inactive while B is canonical |
| Change payload under the same occurrence key | Visible conflict; no invented extra transfer |
| Observe a null block hash or log index | Pending observation stays outside the included-log key and total |
| Change the canonical hash during backfill | Checkpoint does not advance until the replacement branch is reconciled |
| Reuse an index on another block or network | A distinct scoped occurrence, subject to that branch's status |
For an incident record, save the network, transaction hash, both block hashes, log indices, observation times, canonical comparison and applied reversals. The useful conclusion is specific: “T has two active logs in B totaling 12 units; A's two occurrences are retained but inactive.” That statement distinguishes the transaction you searched for from the events your current ledger actually counts.
