Skip to content

fix: harden non-deterministic eth_call reads to prevent silent data corruption (#248) - #256

Open
lukiod wants to merge 2 commits into
livepeer:mainfrom
lukiod:fix/non-deterministic-eth-call-hardening
Open

fix: harden non-deterministic eth_call reads to prevent silent data corruption (#248)#256
lukiod wants to merge 2 commits into
livepeer:mainfrom
lukiod:fix/non-deterministic-eth-call-hardening

Conversation

@lukiod

@lukiod lukiod commented Aug 23, 2026

Copy link
Copy Markdown

Summary

Fixes the root cause of POI divergence documented in issue #248: eth_call state reads that return wrong-but-valid values (from a degraded or lagging RPC backend) silently corrupt indexing state. The existing PR #249 fixed the abort on null Pool but did not address the silent corruption from bad RPC responses.

Changes

roundsManager.tsnewRound() handler

  1. Use try_getNextTranscoderInPool() instead of the bare call (case 5): A reverted eth_call during transcoder enumeration is now logged as log.critical and breaks the loop, rather than silently returning EMPTY_ADDRESS and truncating the pool list.

  2. Upgrade log.infolog.critical for getFirstTranscoderInPool and getTotalBonded reverts (cases 1, 2): Post-launch these reverts are impossible and indicate a determinism hazard (bad RPC backend), not legitimate control flow.

  3. Carry forward protocol.totalActiveStake when getTotalBonded reverts or returns zero (case 2): Previously zeroing the value cascaded into zeroed participationRate and numActiveTranscoders for the entire round.

helpers.tsgetPriceForPair()

  1. Cache last-known non-zero pool prices (DAI/ETH, LPT/ETH) in module-level variables (case 3): On eth_call revert, the cached price is carried forward instead of silently returning ZERO_BD, which would permanently under-count USD volume in 7 accumulators.

  2. Only cache non-zero prices to avoid persisting the "pool not yet deployed" sentinel as the last-known value.

Verification

  • yarn codegen succeeds
  • graph build compiles without errors
  • Existing test suite (docker compose -f docker-compose.test.yml up) requires a local graph-node deployment

References

@lukiod
lukiod requested a review from rickstaa as a code owner August 23, 2026 14:28
lastKnownLptEthPrice is a module level variable, reset to zero on every
subgraph restart since the WASM instance is recreated. A revert on the
first price fetch after a restart fell through to a silent zero, the
same corruption this PR sets out to fix. protocol.lptPriceEth is an
entity backed field already written once per round in
roundsManager.ts, so it survives restarts and is a strictly safer
fallback than the in memory cache alone.
@lukiod

lukiod commented Aug 23, 2026

Copy link
Copy Markdown
Author

Pushed a fix (dd65b9e) and have one thing worth a second look before merge.

Fixed: LPT/ETH price cache did not survive a restart

flowchart LR
    A[slot0 reverts] --> B{in memory cache?}
    B -- yes --> C[use it]
    B -- no, just restarted --> D{protocol.lptPriceEth?}
    D -- yes, new --> E[use it]
    D -- no --> F[fall back to zero, old bug]
Loading

lastKnownLptEthPrice resets to zero on every restart (fresh WASM instance). Added the D/E step, reusing the entity backed field already written each round in roundsManager.ts. yarn codegen and graph build both pass.

Worth a look: log.critical on cases 1 and 5

flowchart LR
    R[eth_call reverts] --> Q{which case}
    Q -- "2, 3, 4" --> G[carry forward last value, indexing continues]
    Q -- "1, 5" --> H[log.critical, subgraph halts, needs manual redeploy]
Loading

log.critical stops the whole indexer, not just this call, see graphprotocol/graph-node#4902. Cases 2 to 4 degrade gracefully on the same kind of revert; 1 and 5 hard stop instead, on the assumption that revert is "impossible post launch." If that ever turns out wrong once, the blast radius is total outage instead of one off bad data. Worth making a deliberate call rather than an implicit one. Not blocking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant