Reorg-Safe RPC Reads: Bind State to a Block Hash

AKZ7...apD2
23 Oct 2026
30

Conceptual illustration: dependent reads share one selected block identity while the surrounding chain can change.
Two successful RPC responses can describe a state that never existed. A backend reads one account, the chain reorganizes, and a second request reaches a different provider. Both endpoints return valid values. The application combines them and publishes an invalid conclusion.
Consider a synthetic balance view at height 900,100. On branch A, two application balances are 12 and 3 units. On replacement branch B, they are 9 and 6. Each branch totals 15. Reading the first value from A and the second from B displays 18. These numbers illustrate mixed-state assembly, not a customer incident or an observed Ethereum transaction.
Pinning the height does not remove the ambiguity: a different block can occupy that height. Sending both requests in one JSON-RPC batch does not establish snapshot isolation either. The application needs an explicit read context and an acceptance rule for the entire dependent result set.
The protocol below is a proposed backend design for Ethereum-style state queries. It binds dependent reads to one block hash, preserves their provenance and stops instead of silently falling back to a weaker selector. It does not claim that any provider has passed these checks or that a successful historical read authorizes a transaction now.

Make the read set the unit of acceptance

Start with the conclusion the application intends to publish. A portfolio total may depend on several balances. An eligibility decision may require ownership, configuration and a contract flag. The useful boundary is that dependent set, not an individual HTTP request.
Give each attempt an immutable envelope:

  • Network scope: expected chain identity and the deployment or genesis context needed to distinguish the intended network.
  • Selected block: hash, height and the header observation used to choose it.
  • Selection policy: the requested head category and the application's freshness requirements.
  • Read specification: named operations, complete parameters and dependencies between them.
  • Execution context: relevant call fields and any allowed overrides, together with the interpretation or decoder revision.
  • Provenance: endpoint identity, attempt identifier and response identifiers without credentials.
  • Completion: required results, observed errors and the decision to accept, hold or abandon the set.

The envelope is an application artifact, not a field that Ethereum nodes store for a caller. The implementation must carry it through queues, caches and provider adapters. If a worker reconstructs missing context from the current head, it has started a different attempt whether or not the request identifier stayed the same.
Define completeness before dispatch. A missing optional display field need not block an unrelated view, but an ownership decision cannot quietly drop an ownership check. Record which fields are mandatory for each conclusion. Partial availability is a product decision, not something the transport library should infer from whichever requests returned first.

Choose a head, then resolve its identity

The Ethereum JSON-RPC reference distinguishes block numbers from moving selectors such as latest, safe and finalized. Those selectors help express a head-selection policy. They are not durable identifiers for a multi-request workflow.
Resolve the selected policy head to a concrete block header and retain its hash. Use that identity for the dependent state reads. Resolving finalized separately for every request can select different finalized blocks as the head advances. That may be acceptable for independent displays, but it is not the single-context contract described here.
The same distinction applies to a numeric height. Height tells an operator where to look; hash identifies the selected block. Re-reading the header after each number-based state query is not equivalent to selecting by hash. A branch can change between observations, including a transition away from a block and back again that surrounding checks fail to reveal.
Choose freshness and finality policy separately from identity. A recent block can be too provisional for a particular operation. A finalized observation can be too old for another operation's freshness limit. Binding all reads to either block answers the coherence question, not whether that block is suitable for the business decision.
Do not transfer Ethereum head semantics automatically to another network.
Review the target chain's execution and settlement model, the endpoint's supported tags and the operator's risk policy. A universal confirmation count would hide the decision the application actually needs to make.

Use the hash object on the methods that support it

EIP-1898 adds a block-parameter object to six state methods: eth_getBalance, eth_getStorageAt, eth_getTransactionCount, eth_getCode, eth_call and eth_getProof. Its hash form carries blockHash and may also carry requireCanonical. Verify support for each method and endpoint used by the application.
In schematic notation, the selector is { "blockHash": H, "requireCanonical": true }, where H must be replaced with the selected block's actual hash. This notation is not executable JSON. Place the object in the method's block-parameter position; do not append it as an unrelated extra argument or assume every RPC method accepts it.
The proposal's author explains the coherence objective:

Multiple sequential queries will query the same state, enabling the application developer to not worry about inconsistencies in their view of the blockchain state.

Charles Cooper, author of EIP-1898, "EIP-1898: Add blockHash to defaultBlock methods," Ethereum Improvement Proposals, created April 1, 2019. Original source.
Apply that statement to a fixed queried block, not to unrestricted future execution. With requireCanonical omitted, its default is false. A retained orphaned block may therefore be queryable without being suitable for a current canonical-chain decision. That behavior can be useful for investigation, but the investigative result needs an explicit noncanonical context.
With requireCanonical true, the responder should reject a known block it considers noncanonical. The check belongs to that responder at request processing time. It neither locks the chain nor proves that every other provider shares the same view.

Do not confuse request consistency with provider trust

A hash selector narrows what the application requested. It does not turn an arbitrary endpoint into a cryptographically verified source of every returned value. The backend still needs a documented provider trust model and controls for unexpected network or response behavior.
State methods generally return the requested value, not a block hash attached to every result. Carry the request envelope beside the response and verify that the adapter actually transmitted the intended selector. A response cannot repair an adapter that silently converted the object to a height or latest.
Review the whole call context for eth_call. Destination, calldata and sender can affect execution. So can relevant gas or value fields and any supported overrides. Two calls against the same block need not be equivalent if their call objects differ. A deterministic read contract must specify the inputs it relies on instead of treating the block hash as its entire identity.
Capabilities also include historical-state availability. A node may know a block header while lacking the state needed to execute a historical query. Do not classify every historical execution failure as proof that the block never existed or became noncanonical. Retain the actual error and investigate the missing capability.
For backend integrations with these dependencies, Pharos Production's blockchain engineering service includes RPC and indexer infrastructure. Make preservation of the read envelope an explicit delivery requirement. The service scope is relevant evidence of fit, not evidence that this proposed protocol has already been validated for a client.

Treat batching as transport, not a database transaction

The JSON-RPC 2.0 specification allows batch members to be processed concurrently and responses to arrive in a different order. A batch reduces transport overhead; its format does not promise an atomic state snapshot.
Match each response to its request identifier. Check that every required identifier appears exactly as expected, with no duplicate or unexplained result. Never zip response-array positions to the original request list. A valid numeric value attached to the wrong field can survive ordinary schema validation and corrupt the final conclusion.
Every relevant batch member still needs the selected hash in its own supported parameter. One member without it can reintroduce a moving-head read into an otherwise pinned set. Review generated requests after SDK or provider-adapter changes, not only the code that constructed a shared options object.
Sequential dependencies need the same discipline. If the first call returns an address used by the second, propagate the original envelope when constructing the second request. Do not resolve the head again because the later request was created in a different function or worker.
An optional third result can be absent under an explicitly defined display contract. A required third result cannot be replaced with its previous value simply because the first two succeeded. Acceptance checks operate on semantic dependencies, not on a blanket rule that a batch with some successful members is good enough.

Restart a context, not just the failed member

Suppose attempt R-17 selected H_A. A balance read succeeds, but a dependent contract call later fails because the responding node no longer considers H_A canonical. Keep the successful response for diagnostics. Do not combine it with a new call against H_B and call the original attempt complete.
If the policy abandons H_A and selects H_B, create a successor attempt. Recompute the required dependent set under H_B, including inputs used to construct later calls. Preserve the predecessor relationship and the reason for abandoning R-17. That history explains why apparently successful work was not published.
Retrying the same operation against the same hash is different. A bounded retry after a transient transport failure can remain inside the attempt when all relevant parameters stay fixed and the acceptance policy still permits it. Count the retry and retain endpoint provenance. Do not let retries continue beyond the application's freshness or operational deadline.
Provider failover should first ask whether the replacement endpoint can serve the same hash with the required method semantics. If it can, the attempt may continue under the declared trust policy. If it cannot, a fallback to a moving tag is a context change, not transparent resilience.
Late responses deserve an explicit rule. A result from abandoned R-17 must not fill a missing field in successor R-18 merely because the account address and method match. Attempt membership belongs in the aggregation key and completion check. Cancellation reduces wasted work; it is not proof that no old response will arrive.
Proposed read-set protocol. H_A and H_B are illustrative labels, not real hashes; a context change starts a new attempt.

Keep failure classes separate

Use a failure matrix whose actions preserve the read contract:

  • Unsupported parameter shape: stop that provider path, review capability and select a validated route. Do not strip the hash object automatically.
  • Block not found: retain the queried hash and endpoint, distinguish absence at this responder from a global chain conclusion, then apply the retry or hold policy.
  • Known noncanonical block: abandon canonical-decision acceptance for that attempt; retain its observations and begin a separately identified attempt if policy permits.
  • Historical state unavailable: find an approved endpoint with the needed history or hold. A header-only check does not supply the missing state.
  • Transport timeout or rate limit: use a bounded retry with unchanged context, or fail over without changing the hash.
  • Execution or decoding error: preserve the call inputs and interpretation revision. Do not assume a different head will make the original business operation valid.
  • Incomplete or mismatched response set: reject assembly and investigate the missing, duplicate or incorrectly correlated member.

EIP-1898 recommends distinct errors for an unknown block and a known noncanonical block, with the unknown-block check taking precedence. Its recommended numeric codes are not a substitute for verifying actual endpoint behavior. Preserve the error object and the classified reason rather than hard-coding every provider into an undocumented message-string match.
An unclassified error should remain unclassified until evidence resolves it. A catch-all reorg label makes transport outages look like chain events and can trigger unnecessary recomputation. A catch-all retry can conceal a permanently unsupported selector. Both lose the operational distinction the matrix exists to preserve.

Bind caches to the execution context

A cache keyed only by method and address can return a value from another block. Adding the height still allows competing blocks at the same height to collide. Include the network scope, selected block hash and normalized method parameters in the cache identity.
For eth_call, normalize the complete supported call envelope without discarding meaningful fields. Treat missing defaults carefully: an adapter change that starts supplying a sender can change the call's behavior. Include overrides where they are permitted, and distinguish decoded representations when decoder revisions can alter interpretation.
Separate immutable historical content from mutable eligibility. The fact that a response was obtained for H_A does not change when H_A is orphaned. Whether that response may satisfy a current canonical-decision policy does change. A cache hit must not inherit an indefinitely valid canonicality assertion from its original fetch.
The same issue arises with negative entries. An endpoint's temporary inability to find a block should not become a permanent global claim that the block does not exist. Give failure entries a defined scope and expiry, preserving the responder and observation time.
Keep credentials and private infrastructure details out of exported evidence. A useful handoff can identify a provider configuration revision without copying access tokens or account-specific endpoint secrets. Review what request logging retains before turning a diagnostic bundle into a customer-facing attachment.

Make disagreement visible before it becomes a fallback

Hedged requests create another boundary. If two endpoints answer the same operation, do not automatically take the first value when the second reports a conflicting outcome. Define whether the second request is only a latency optimization or an independent observation required by policy. Those arrangements have different completion conditions.
Two providers returning the same value are not automatically independent evidence. They may share upstream infrastructure, and an agreement policy needs a documented reason to trust its inputs. Conversely, a disagreement does not identify the dishonest or broken party by itself. Compare the network, selected hash, complete call context and endpoint capabilities before assigning a cause.
Expose separate operational measures for unavailable historical state, canonicality rejection, unsupported selectors and transport failures. Count abandoned read sets as well as successful member requests. A dashboard showing high RPC success can conceal an aggregator that repeatedly fails to assemble one eligible decision.
Retain the age of the selected context at acceptance and the policy reason for any hold. Alerting should identify the blocked consumer and responsible operator without dumping sensitive request parameters into a public channel. Review ownership matters here: a provider-health alert cannot decide whether a product may publish a partial balance view.

Logs use a different hash filter

Do not apply the EIP-1898 state selector mechanically to eth_getLogs. EIP-234 defines blockHash in the log filter, as an alternative to fromBlock and toBlock. Those fields are not a second version of the state-method parameter object.
Hash-scoped log retrieval resolves an important empty-result ambiguity. A successful empty result for an explicitly selected block has a clearer identity than an empty result selected only by a height that may now refer to another branch. It still says only that no logs matched the requested filter for that block according to the responder.
It does not establish that the address and topic filter covered everything the application needed. Nor does a log query reconstruct an arbitrary contract state that the contract never emitted. Keep collector completeness, decoder interpretation and state-query coherence as separate review questions.
When a workflow combines logs and state, record how their contexts relate. State queried at a block describes the state at that block, not automatically the state immediately before a particular transaction within it. An event consumer that needs an intrablock precondition requires additional execution evidence. Matching a block hash alone does not answer that timing question.

Accept a historical view without promising a future action

After all required reads complete, evaluate the set against its acceptance policy. Check context identity, completeness, interpretation and freshness. Where policy requires a current canonicality observation, obtain and retain it through a supported mechanism, with the same provider-trust limitations stated earlier.
Such an observation narrows uncertainty at that boundary. It does not remove the time gap between checking and using the result. A reorganization, new block or ordinary contract update can occur before an eventual transaction executes. An RPC-read protocol cannot by itself reserve ownership, freeze a price or guarantee authorization at execution time.
Treat any consequential action as a separate controlled workflow. Its on-chain preconditions, simulation policy, authorization and failure handling must match the actual application. This article specifies neither a custody operation nor permission to broadcast a transaction. A coherent read view is an input to that design, not its substitute.
Publish the context with the derived view where consumers need it: selected hash, observation time and policy status, rather than an unexplained current label. If later evidence invalidates eligibility, append a correction or withdrawal record. Preserve what the system actually observed and identify which downstream exports or caches consumed it. Updating the backend's latest row does not recall a report already downloaded by another team. The correction path needs its own owner and consumer-notification rule; it should not erase the original evidence to make the history look consistent.
Pharos Production's blockchain delivery process lists production-readiness monitoring, runbooks and rollback. Use the read-set failure matrix as a concrete acceptance artifact within that scope. Ask which component holds publication, who can approve a context change and what evidence allows work to resume; do not accept a generic availability claim as the answer.

Rehearse refusal before the provider switch

Run the following proposed exercises against the actual adapter and aggregation implementation in a controlled environment. They are acceptance expectations, not results of a test executed for this article.

  1. Advance the policy head between dependent calls and verify that an existing attempt retains its original hash.
  2. Replace a block at the selected height and verify that height equality does not authorize mixing branch values.
  3. Make the hash object unsupported and confirm that the adapter fails closed instead of substituting latest.
  4. Return batch responses out of order, then introduce a duplicate identifier and verify the distinct correlation outcomes.
  5. Fail over to an endpoint that knows the header but cannot execute the historical state query; preserve that failure class.
  6. Abandon an attempt, deliver its successful response late and confirm that it cannot complete the successor.
  7. Reuse a cached response after its canonicality observation becomes invalid for policy and verify that eligibility is reconsidered.
  8. Change a meaningful eth_call field and confirm that the cache and evidence no longer describe it as the original request.

Capture the command, adapter build, controlled inputs and observed outputs when those exercises are actually run. A written checklist does not prove that an SDK preserved a parameter or that a production provider retained the required history.
The receiving team should be able to reconstruct one accepted set and one abandoned set from their envelopes. That is the handoff criterion: not merely that every request returned something, but that the application can explain which state supported its conclusion and why conflicting results were refused.

More insights to read

  1. Verify an Indexer Can Recover from a Chain Reorganization
  2. A Smart Contract Upgrade Needs an Evidence Bundle
  3. Your Contract Upgraded. Can the Indexer Still Read Its Events?
  4. Smart Contract Upgrade Authority: Timelocks, Multisigs, and Emergency Controls
  5. 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.

BULB: The Future of Social Media in Web3

Learn more

Enjoy this blog? Subscribe to Dmytro Nasyrov

0 Comments