Event Schemas Change Too: Versioning an Indexer Across Proxy Upgrades
Conceptual illustration: one event stream can require different interpretation rules after an upgrade.
A proxy can keep its address while the meaning of its events changes. Replacing the ABI in an indexer's configuration may make new receipts readable and silently reinterpret old ones. The harder failure appears when an upgrade is reorganized out: the indexer removes the orphaned business rows but keeps the decoder activation that authorized their interpretation.
The data branch has rolled back. The interpretation branch has not.
Treat a decoder registry as versioned production state, not a folder containing the latest contract artifacts. Each accepted log needs an explanation connecting its raw receipt, the relevant execution boundary, an immutable schema and the projection that used it. If that explanation depends on a replaced block, reconsider the interpretation before publishing replacement rows.
This article develops a proposed registry and cutover procedure for EVM application events behind a reviewed proxy. Its branch labels, block coordinates and release records are illustrative, not customer incidents or observed chain transactions. It does not report an executed deployment test. The deliverable is an interpretation manifest that a receiving team can inspect and challenge.
Keep the wire schema separate from business meaning
For a non-anonymous event, the signature topic derives from the event name and ordered canonical parameter types. Parameter names and indexed modifiers are not part of that signature. Moving an address between indexed and non-indexed positions can therefore change how topics and data must be read without changing the signature topic.
The Solidity ABI specification defines that separation. An old decoder can return valid-looking addresses in the wrong roles. A successful decode establishes compatibility with the decoder's assumptions, not agreement with the emitting implementation.
Keep the warning short but apply it rigorously. Record the full event fragment, including indexed flags and anonymous status. Record field meaning separately: whether an integer represents base units, a cumulative total or a change. Two releases can preserve the entire wire layout while changing that interpretation.
Maintain a third identity for the derived projection. A decoder might correctly extract an amount while a projection applies an outdated accounting rule. Replacing the ABI will not repair a projection bug. Conversely, a projection migration need not imply that the on-chain event changed.
For delegated execution, code runs in the calling contract's context; the Solidity delegatecall explanation describes this distinction. Ordinary delegated application logs identify the proxy emitter, not a convenient new address for each implementation. Scope collection to the actual execution path; do not assume every log in an upgrade transaction originates through the same delegatecall.
These boundaries suggest separate version fields for event layout, business interpretation and projection code. A single release label can group them, but cannot substitute for their individual identities.
Review semantic changes even when a compiler produces identical event declarations. If a contract starts emitting a gross amount where downstream code expects a net amount, neither topic matching nor ABI decoding establishes the intended accounting treatment. Require the release owner to identify changed field contracts and the consumers affected by them. A field name is documentation to inspect, not independent proof that its meaning stayed constant.
Turn the activation block into an execution interval
Suppose a reviewed upgrade appears on branch A at synthetic block 81,200, transaction position 3. An earlier transaction at position 2 uses the old implementation. A later transaction at position 5 uses the new implementation. Assigning the new decoder to every log at or after block 81,200 misclassifies the earlier transaction.
That is a counterexample to a height-only rule, not a recommended confirmation depth.
Use an ordered execution cursor. Retain block number for navigation, block hash for branch identity, transaction index and log index. The Ethereum JSON-RPC documentation exposes these receipt/log coordinates. Its logIndex is a position within the block, not a counter that should silently restart for each transaction.
For this fixture, the reviewed intervals are old interpretation before transaction 3, unresolved interpretation inside transaction 3, and new interpretation after transaction 3. Describe lower and upper bounds explicitly. An inclusive comparison in one worker and an exclusive comparison in another can turn one boundary event into either a duplicate or an omission.
The transaction containing the upgrade is deliberately unresolved here. Initialization, migration calls or additional delegatecalls may produce application logs. Receipt order alone does not prove which implementation produced every log. Obtain execution evidence for the actual deployed path before refining that interval.
When upgrade semantics and backend ingestion cross this boundary, Pharos Production's blockchain engineering scope includes supporting indexers and RPC infrastructure. Make the reviewed activation predicate a requested delivery artifact. The service scope does not itself prove that a particular deployment's boundary has been validated.
Give each interpretation an immutable manifest
A useful manifest is small enough to review and specific enough to reconstruct one decision. Avoid storing a mutable explorer ABI URL as its sole schema reference. Preserve the artifact bytes and digest alongside the decoder build that consumed them.
Use these fields as a proposed record:
- Scope: chain identity, emitter address and the proxy mechanism actually reviewed.
- Execution interval: lower and upper cursor bounds, inclusive or exclusive conventions, and the treatment of the upgrade transaction.
- Branch evidence: the activation receipt's block hash, transaction identity and the history against which it was checked.
- Code and schema: implementation identity, relevant code evidence, complete event fragment and immutable ABI digest.
- Interpretation: field roles, units, semantic revision and decoder build digest.
- Projection: transformation revision, destination generation and replay checkpoint.
- Decision: reviewer, evidence references, approval state and the reason for any quarantine.
An ABI digest proves which bytes were selected. It does not prove those bytes describe the executing code. A verified source artifact, deployment record or other appropriate evidence must make that connection. Likewise, an implementation address by itself says little about how a custom proxy or beacon reached that code at a historical cursor.
Keep revisions append-only. A corrected interval receives a successor record referencing the earlier decision; it does not erase why yesterday's projection selected a different decoder. A mutable operational index can point to the accepted revision while the decision history remains intact.
Distinguish review from activation. A reviewed candidate can still depend on a branch that is no longer canonical. Record that dependency so publication eligibility can be recomputed, rather than treating approval as a permanent property of a block number.
Hash a defined artifact representation. If the retained object is the compiler's exact ABI JSON file, hash those bytes and preserve them. If a tool normalizes JSON before hashing, retain the normalization rule and normalized bytes as well. Reformatting a file can change its byte digest without changing its event meaning. That is not automatically a contract upgrade, and a digest comparison should not trigger a new interpretation interval by itself.
Operational states need equally precise definitions. Candidate means the evidence is being assembled; reviewed means the named reviewer accepted its scoped predicate; eligible means the current branch observations support using it. Quarantined names an unresolved interval, while orphaned describes evidence invalidated for canonical publication. Do not let a status field stand in for the evidence it summarizes. A correction can supersede a reviewed decision without erasing either its previous use or its consequences.
An upgrade notification is evidence, not the whole predicate
ERC-1967 standardizes proxy storage locations and recommends corresponding notifications when relevant slots change. Its authors explain the monitoring motivation:
Monitoring of proxies is essential to the security of many applications.
Santiago Palladino, Francisco Giordano and Hadrien Croubois, ERC-1967: Proxy Storage Slots, Ethereum Improvement Proposals, created April 24, 2019.
Use that signal to find and inspect a change. Do not turn a recommendation into a claim that every deployed proxy emits the expected notification or that its location defines every application event's decoder.
For a direct implementation-slot proxy, reconcile the notification with the reviewed implementation transition. For a beacon, the relevant implementation dependency is different. A custom dispatcher may require selector-specific execution evidence. This article's simple interval model must be extended before it is applied to those mechanisms.
Historical state queries also have limits. Reading implementation storage at the end of an upgrade block cannot resolve every earlier transaction in that block. Confirm the provider's historical-state capabilities and the coordinate a query actually represents. A current explorer view is not a historical execution trace.
If evidence is incomplete, preserve a candidate manifest with unresolved scope. Do not promote it because its ABI successfully parses a sample. The failure state should name the missing evidence, such as upgrade-transaction execution order, instead of hiding uncertainty behind a generic decode error.
Revoke branch-dependent activation when its evidence is orphaned
Return to branch A. Its block label H_A denotes the block containing the reviewed transition; these labels are not real hashes. The indexer has accepted the new interpretation for relevant later events on that observed branch.
Now the canonical history replaces that block with H_B at the same height. Branch B's reviewed execution contains no equivalent transition, and the known earlier implementation remains applicable in this illustrative scenario. That last condition is an explicit fixture assumption, not something established merely by failing to find an upgrade notification.
Three records need reconsideration: business logs from the replaced block, the activation evidence attached to H_A, and projections derived under that activation. Removing only business rows leaves a poisoned registry entry that can misdecode replacement logs.
Mark the H_A activation as orphaned for canonical publication while retaining its evidence for audit. Rebuild the eligible interval map from the accepted branch history. Do not carry its height-based successor rule into H_B. A replacement activation requires its own reviewable evidence.
The expected routing differs even when height and transaction position coincide. Branch A's transaction 5 uses the new interpretation under the stated evidence. Branch B's transaction 5 uses the earlier interpretation under its separately reviewed history. If that review is unavailable, the replacement event remains quarantined.
This is not an instruction to discard finalized data on any provider disagreement. Define a chain-specific canonicality/finality policy outside the example, verify the observed history and investigate conflicting providers. The registry must expose what its decision depends on, including observations that remain provisional.
The dependency is an ancestry relationship, not equality between two block hashes. A log in a later block will naturally have a different hash from the activation block. The question is whether the accepted history for that log contains the reviewed activation and preserves its execution predicate. Store enough linked evidence to answer that question at the required review boundary. A database join requiring the business log's hash to equal H_A would reject valid descendants and still fail to model the actual dependency.Illustrative branch replacement at height 81,200. Transaction 3 on branch A stays quarantined; branch B needs its own reviewed history.
Quarantine uncertainty without losing the raw event
Separate raw collection from accepted interpretation. Preserve the emitter, complete topics and data, transaction and block identities, coordinates and the acquisition provenance needed to investigate discrepancies. Keep branch observations rather than overwriting one receipt solely because its height matches another.
A proposed raw-log identity includes chain identity, block hash and log index. A separate business key may connect observations to a domain entity, but it should not collapse competing branch evidence before reconciliation. Transaction hashes alone do not identify every log produced by one transaction.
Route a record to quarantine when no eligible manifest covers its cursor, multiple accepted intervals overlap, the expected artifact is unavailable or the branch evidence no longer supports the interval. Do not try every decoder and keep whichever one returns a plausible result.
Also inspect collection filters. A subscription that requests only old signature topics can omit a genuinely new event before decoding begins. An unknown-event queue cannot recover what the collector never requested. Review address/topic coverage and backfill the affected interval from an appropriate source.
Indexed dynamic values need particular care. A topic may contain a hash rather than recoverable plaintext. Preserve that representation and its meaning; do not manufacture the original string because a later schema introduces a similarly named text field.
Operational counters should distinguish collection gaps, missing manifests, ambiguous intervals and projection failures. A single lag number cannot explain which permission is missing. Assign a review owner and a next action to each blocked interval so quarantine does not become an unowned archive.
Keep ingestion progress separate from accepted-publication progress. A collector may have retained later raw receipts while the served projection waits at an unresolved upgrade interval. Expose both positions and the blocking reason. Calling the system caught up because its collector reached the latest block would conceal the unreviewed interpretation. Where consumers permit partial availability, define which unaffected partitions can advance; do not infer independence merely because the blocked records have different transaction hashes.
Rebuild in shadow and keep external actions out of replay
Start with a bounded replay window around the activation and any affected historical interval. The window follows the evidence; there is no universal number of blocks that proves correctness. Identify its start state, end checkpoint, raw-input set and selected registry revision.
Build a separate projection generation. Preserve the currently served generation while the candidate is calculated. Compare identified entities and event effects, not only row totals. Equal counts can hide reversed address roles, altered units or one missing row offset by a duplicate.
For each intentional difference, keep an explanation tied to a raw-log identity and interpretation record. For each unexplained difference, stop promotion. A reviewer should be able to distinguish a corrected mapping from an accidental change caused by a different input window.
Replaying historical events must not resend customer notifications, repeat transfers or reissue external instructions. Separate derived-state reconstruction from action dispatch. If historical external effects already occurred under a wrong interpretation, repairing them is a separate operational process with its own authority and evidence.
Do not promise exactly-once behavior merely because a projection uses an upsert. Retries, checkpoint placement, unique keys and downstream action handling determine the actual failure surface. Test the chosen implementation at the boundaries it can fail, rather than importing a reliability label from the database operation.
Pharos Production's smart-contract development service is relevant when a contract release changes what downstream software must interpret. Specify retained raw inputs, decoder manifests and replay acceptance evidence in the engagement scope. These are requested deliverables here, not a claim that this proposed procedure has already been executed for a client.
Publish the registry and projection as one compatible generation
Two independent configuration changes create an avoidable mismatch window. The registry switches to the new interpretation while an API still reads an old projection, or the projection switches while a worker continues selecting an earlier decoder.
Publish a release descriptor that binds the accepted registry revision, decoder build, projection generation and checkpoint. Readers should resolve a compatible descriptor rather than independently requesting the newest value of each component.
Where the storage design supports it, commit the projection checkpoint and active-generation pointer under the appropriate atomic boundary. Otherwise define an explicit staged cutover protocol and test its intermediate states. A sentence in a runbook is not a substitute for the actual consistency mechanism.
Keep the prior compatible descriptor available for a reviewed rollback. Rolling back application code alone may leave derived state written under different semantics. State whether rollback restores an earlier generation, rebuilds from raw data or performs an approved correction; those actions have different costs and evidence requirements.
Give downstream readers a generation identity they can retain with their own exports or caches. During validation, compare responses under the candidate descriptor with the previously served descriptor using the same checkpoint. Without that alignment, a moving input window can look like a decoder difference. Document how clients invalidate or reconcile cached rows after a correction; switching the server's pointer does not recall copies already consumed elsewhere.
Handle interrupted promotion explicitly. A crash after building a candidate but before switching the pointer should leave the prior generation served. A crash after a completed switch must not let a restarted worker infer a different registry from a mutable default. These are proposed acceptance properties, not results of an executed crash test.
Record the operator who can promote, the conditions that block promotion and the evidence required to resume. A pending review must not silently expire into acceptance. Time passing does not supply the missing interpretation proof.
Exercise the boundary, not just the decoder's happy path
Use distinct values and explicit expected outcomes. The following checks form a proposed acceptance set for the illustrative registry, not a completed conformance suite:
- An earlier event in block 81,200, transaction 2 selects the old interpretation on branch A.
- An application event in transaction 3 stays quarantined until its actual execution path has been reviewed.
- A later event in transaction 5 selects the new interpretation only under branch A's eligible activation evidence.
- Replacing H_A with H_B revokes the orphaned activation's canonical-publication eligibility, not its retained audit record.
- The replacement branch's transaction 5 follows its separately reviewed history or remains quarantined when that evidence is absent.
- Overlapping accepted intervals fail selection rather than choosing a decoder by list order.
- An unfamiliar signature is retained when collection scope includes it; a deliberately restricted filter produces an explicit collection-gap check.
- Replaying the same retained input does not duplicate a projection effect or dispatch an external action.
- An interrupted generation switch preserves a coherent served descriptor at the supported atomic boundary.
- A rollback reconstructs the declared compatible state and leaves a record of why promotion was reversed.
Run representative checks against the actual collector and storage implementation. A helper that routes hand-written cursors cannot establish provider completeness, transaction execution order or durable recovery. Preserve the test command, relevant build identities and observed results when those tests are actually performed.
Hand off an explanation that survives the next release
Choose an event before the boundary, one inside the upgrade transaction and a replacement-branch observation. Ask the receiving team to reconstruct each decision using retained evidence rather than the original author's memory.
The handoff record should identify the raw receipt, selected manifest or quarantine reason, branch dependency, decoder build, semantic interpretation and served projection generation. It should also identify who can approve a correction and which consumers need notice when already-published meaning changes.
Retention matters as much as the immediate release. Keep the ABI bytes, decoder source or reproducible build reference, projection migration and branch evidence for the reconstruction period your application requires. A link to a disappearing build artifact makes an immutable digest operationally useless.
The readiness question is not whether the newest receipt decodes. It is whether the team can explain why that interpretation was eligible at that execution point, revoke it when its evidence is invalidated and reconstruct the compatible projection without repeating external effects. That explanation is the versioned contract between the proxy release and the indexer.
More insights to read
- Your Contract Upgraded. Can the Indexer Still Read Its Events?
- A Smart Contract Upgrade Needs an Evidence Bundle
- Smart Contract Upgrade Authority: Timelocks, Multisigs, and Emergency Controls
- Verify an Indexer Can Recover from a Chain Reorganization
- Smart Contract Release Audit: 8 Repository Artifacts
About the author
Dmytro Nasyrov. Photo supplied by the author.
Written by Dmytro Nasyrov PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.
