3MIKAN
仮想通貨直コン

Ethereum RPCのlatestがずれる理由:fixed block・archive・Multicall

Ethereum RPCのpending・latest・safe・finalized、block number/hash、archive state、JSON-RPC batch、Multicallを比較。同一blockで価格・残高・slot・logsを再現します。

3MIKANのブランドキャラクターが動くblock frameの一つを固定し、同じ時点の価格・残高・storageを照合するEthereum RPCの記事画像

Ethereum RPCで同じaddressを読んだはずなのに、価格、残高、allowance、proxy slot、eventが噛み合わない。最初に確認するのはprovider名ではなく、すべてのreadが同じblockを参照したかです。

latestは固定snapshotではありません。1回目のeth_callと2回目のeth_getLogsの間にheadが進めば、どちらも正しい応答でも、組み合わせた結果はchain上の一つの時点に存在しません。

対策は、目的に合うsafeまたはfinalizedを一度取得し、そのblock numberとhashを保存してから全methodへ渡すことです。JSON-RPC batchやMulticallはrequestをまとめますが、block指定の代わりにはなりません。

この記事ではlocal Anvilでmoving latestの失敗を再現し、Ethereum mainnet block 25,869,830へ価格・残高・allowance・raw storage・logsを固定します。公開RPCの観測は2026-08-31、Japan region、public unauthenticated planです。wallet、API key、署名、transaction送信、資産操作は使いません。

pending・latest・safe・finalizedは位置が違う

Ethereum JSON-RPCのblock parameterは、数量で指定するblock numberのほか、pendinglatestsafefinalizedなどのtagを受け取ります。

identifier 何を選ぶか 主な用途 注意点
pending 次のblock候補に対応するpending state mempool反映後のシミュレーション 内容が変わる。hashがnullの実装もある
latest nodeが現在canonical headとみなすblock 最新表示、低遅延監視 callごとに進み、近いreorgの影響を受ける
safe consensus layerがsafeとするhead latestより強い基準の表示 method・providerの対応差を確認する
finalized finalized checkpointに対応するhead 再現用blockの選択、会計・記事data latestより古い。極端なconsensus failureまでの絶対保証ではない
block number その高さのblock 複数methodを同じ高さへ揃える non-finalizedではreorg後に別hashへ対応し得る
block hash そのblock identity 厳密な再現、canonicality確認 methodがEIP-1898へ対応するか確認する

2026-08-31 03:46 JST前後に3つのpublic endpointを確認したsnapshotでは、pending 25,869,973、latest 25,869,972、safe 25,869,926、finalized 25,869,894でした。2 endpointはpending blockのhashをnull、1 endpointは暫定hashを返しています。

これはprovider優劣の表ではありません。同じ時刻帯でもtagが指す位置と返却fieldが異なり得るため、取得日時、endpoint、plan、region、number、hashを一緒に保存する例です。

Ethereum RPCのpending、latest、safe、finalizedと固定blockの位置関係。moving headから一つのblock numberとhashを固定する

finalizedは強いが「何があっても不可逆」ではない

Ethereum proof-of-stakeでは、validatorのsupermajority voteによりcheckpointがfinalizedされます。通常の短いreorgと比べて、finalized blockを覆すには大規模なconsensus failureやslash対象となる振る舞いが必要です。

したがって再現用snapshotにはfinalizedが扱いやすい一方、「数学的に絶対不可逆」「providerがfinalizedと返せば必ず正しい」とは書けません。別endpointでnumberとhashを照合し、観測条件を残します。

blockを一度解決して全readへ渡す

viemでは、最初にfinalized blockを一度だけ取得します。その後はblockTagを繰り返し使わず、解決済みのblock.numberを全readへ渡します。

const block = await client.getBlock({ blockTag: 'finalized' })
if (block.number === null || block.hash === null) {
  throw new Error('finalized block identity is unavailable')
}

const at = block.number

const price = await client.readContract({
  address: feed,
  abi: feedAbi,
  functionName: 'latestRoundData',
  blockNumber: at,
})

const balance = await client.readContract({
  address: token,
  abi: erc20Abi,
  functionName: 'balanceOf',
  args: [holder],
  blockNumber: at,
})

const implementation = await client.getStorageAt({
  address: proxy,
  slot: implementationSlot,
  blockNumber: at,
})

const logs = await client.getLogs({
  address: token,
  fromBlock: at,
  toBlock: at,
})

block numberを使う利点は、viemの各actionへ同じ形で渡しやすいことです。同時にhashも保存し、別providerから取得したblock headerが同じhashか照合します。

proxyのraw slotを読む手順はERC-1967 proxyをblockへ固定して調べる記事、transaction・receipt・eventのerror層を先に分ける場合はtransaction失敗を切り分ける順番も参照してください。

EIP-1898でblock hashとcanonicalityを指定する

EIP-1898は、block numberの代わりにblockHashを含むobjectを渡せる形を定義しています。requireCanonical: trueを付けると、そのhashがcanonical chain上にない場合をerrorとして扱えます。

const raw = await client.request({
  method: 'eth_call',
  params: [
    { to: feed, data: latestRoundDataCalldata },
    { blockHash: block.hash, requireCanonical: true },
  ],
})

EIP-1898の対象にはeth_getBalanceeth_getStorageAteth_getTransactionCounteth_getCodeeth_calleth_getProofがあります。blockNumberblockHashを同じobjectへ同時に入れず、requireCanonicalはhashと一緒に使います。

今回のfixed block hashは0x53d1375a96ad48b9a0f3e30152c0f01dc142fca8dce2f22ee95270a27b365294です。public endpointとAnvilの両方で、blockHash + requireCanonical: trueeth_callがnumber固定と同じ値を返しました。

ただし、gatewayがEIP-1898を実装していない場合があります。その場合はnumberと取得済みheader hashを別々に照合し、unsupported errorも隠さず記録します。hash指定はarchive stateを生成しないため、古いstateがprune済みならやはり失敗します。

historical eth_callにはstate historyが必要

古いblock headerやtransactionを取得できることと、その時点のcontract stateでeth_callできることは別です。historical callには、対象blockのstateを再構成できるexecution clientまたはarchive serviceが必要です。

Gethのcurrent archive documentationでは、path-based state historyを--history.stateで設定します。現在のcommand-line referenceはdefaultを90,000 blocksとし、0は全historyを要求する値です。設定、sync方式、client version、provider planによって利用可能な深さは変わります。

同じendpointでcurrentは成功、old blockは失敗した

Chainlink ETH/USD feedのlatestRoundData()を、latestとblock 16,000,000で同じpublic endpointへ送りました。

request observed result at 2026-08-31
eth_call(..., "latest") HTTP 200、decoded resultを取得
eth_call(..., "0xf42400") HTTP 403 / RPC code -32602
error Archive requests require a personal token

同じhistorical callは、authoring時点の別のpublic unauthenticated endpointで成功し、ETH/USD answer 120872000000、decimals 8、updatedAt 2022-11-18T21:55:35Zを返しました。

ここから言えるのは、block 16,000,000のstate自体は取得可能だが、最初のpublic planではarchive requestとして拒否されたことです。失敗endpointがarchive非対応のnodeである、別endpointが永久に全historyを保証する、とまでは断定できません。plan gate、retention、routing、rate limitは後から変わり得ます。

sequential・JSON-RPC batch・Multicallの違い

3方式は「何をまとめるか」が違います。

method HTTP / RPC形状 同一blockになる条件 response / failure 対象外
sequential callごとに1 request 全callへ同じnumber/hashを渡す call単位で順番に処理 request途中でlatestが動く
JSON-RPC batch 複数RPC itemを1 HTTP bodyへ入れる 各itemへ同じnumber/hashを渡す orderは保証されずIDで照合 atomic executionの保証
Multicall3 1回のeth_call内で複数contract call outer eth_callへblockを指定 callごとのallowFailureを設計 logs、raw storage、archive state供給

JSON-RPC 2.0 specificationでは、batch内のrequestをserverが並列または任意順で処理でき、response orderもrequest orderと一致する必要はありません。配列位置ではなくidで結合します。

viemのmulticall actionはMulticall3へ複数のcontract readを渡し、1回のRPC callへまとめます。同じouter eth_callなので、contract側のreadは一つのEVM block contextを共有します。blockNumberを省略すれば、その1回が実行された時点のlatestです。後日同じ値を再現したいならexplicit blockを残します。

sequential RPC、JSON-RPC batch、Multicall3のrequest数とsnapshot境界。共通のexplicit block pinと、別RPCになるraw storage・logsを示す

batchだけではatomic snapshotにならない

次のbatchは1 HTTP requestですが、各itemがlatestを指定しています。serverは独立して処理できるため、head境界をまたぐ可能性を仕様上排除できません。

[
  {"jsonrpc":"2.0","id":4105,"method":"eth_call","params":[{"to":"0x…","data":"0x…"},"latest"]},
  {"jsonrpc":"2.0","id":4101,"method":"eth_call","params":[{"to":"0x…","data":"0x…"},"latest"]}
]

latestを両方とも0x18abe06へ変えれば、処理順やresponse順にかかわらず同じblock stateを要求できます。batchがsnapshotを作るのではなく、各itemのblock pinがsnapshotを作ります。

Multicallに入らないstateもある

Multicall3はEVM contract callをまとめます。balanceOfallowance、oracle getter、複数contractのview functionには向きます。

一方、eth_getStorageAtはnodeのRPC method、eth_getLogsはreceipt log indexからの検索です。Multicall contractから同じ形で取得できません。これらは別requestのまま同じblock numberまたはhashへ固定し、結果を後から結合します。

また、Multicallを古いblockへ指定しても、providerがそのhistorical stateを持たなければouter eth_callが失敗します。Multicallはarchive nodeの代替ではありません。

Anvilでmoving latestのずれを再現する

local再現環境はFoundry / Anvil 1.8.0、Solidity 0.8.36、viem 2.56.0、Prague EVM、chain ID 31337へ固定しました。Foundryは3 passed; 0 failed; 0 skippedです。

最初にblock 3で4種類のstateを更新し、price()latestで読みます。その後、別transactionでblock 4へ進めてからbalanceOf()latestで読みました。

read block value
first price() at latest 3 250,000,000,000
state update transaction 4 price / balance / allowance / raw slotを更新
second balanceOf() at latest 4 1,400,000

2つのresponseは個別には正しいですが、price from block 3 + balance from block 4というpairは一つのsnapshotではありません。

次に全callをblock 3へ固定しました。

state expected sequential JSON-RPC batch Multicall
price 250,000,000,000 250,000,000,000 250,000,000,000 250,000,000,000
balance 1,250,000 1,250,000 1,250,000 1,250,000
allowance 7 7 7 7
raw value 41 41 41 41

batchのrequest ID orderは3003, 3001, 3004, 3002です。再現scriptはresponse配列を意図的に逆順へ並べたnormalization probeも通し、IDで結合して同じ4値になることを検査します。

同じblock 3のslot 0は0x…0029、decoded valueは41です。SnapshotUpdated eventのblock hashもblock 3と一致し、EIP-1898 hash-pinned callも41を返しました。local再現JSONにblock、raw slot、event、request orderを保存しています。

mainnetで5種類のstateを同じblockへ固定した

公開計測はEthereum mainnet block 25,869,830、hex 0x18abe06、timestamp 2026-08-30T18:17:59Zを使いました。authoring時点のfinalized headより64 blocks古く、2つのstate-capable public endpointでblock hashと全contract readを照合しています。

state type target / method fixed-block result
oracle price Chainlink ETH/USD latestRoundData() 250019228748 / decimals 8 = 2,500.19228748 USD
token balance USDC balanceOf(OP L1StandardBridge) 25313978911659 / decimals 6 = 25,313,978.911659 USDC
token allowance USDC allowance(bridge, portal) 0
raw proxy storage bridge EIP-1967 implementation slot 0xB37a11AadF167B2F0b8dD85372De4bC66CD4A891
event logs USDC Transfer in the fixed block 14 logs

Chainlink roundのupdatedAt2026-08-30T17:26:11Zです。block timestampより前なのは正常です。oracleが各blockで必ず更新されるのではなく、そのblock時点でcontractが保持していた最新roundを読んでいるためです。

contract getter 6件は、sequential 6 HTTP requests、JSON-RPC batch 1 HTTP request、Multicall3 1 eth_callの3方式ですべて一致しました。別endpointでも同じ6値です。

raw implementation slotと14件のlogsは別RPCですが、同じblock numberを使用しています。14 logsの正規化JSONはSHA-256 b2aa6df269ad18cc0d587f2ac82c1758bada2beaeb550a9a82557c8ec77e839aです。公開計測JSONには全raw log、raw eth_call、batch ID order、EIP-1898 result、archive success / failureを残しました。

USDC addressはCircle公式、Chainlink feedは公式quickstart、OP Mainnet bridge / portalはSuperchain Registryの固定commitから確認しています。addressとABIを推測してprovider比較へ混ぜないためです。

計測scriptを再実行する

defaultはnetworkへ接続せず、保存済みJSONのblock、3方式の値、raw slot、logs、archive境界、endpoint安全条件を検査します。

node scripts/fetch-ethereum-rpc-snapshot.mjs

public endpointへread-onlyで再照合する場合だけ--fetchを付けます。固定block stateがcommitted evidenceと一致するか検査し、動くtagとarchive observationは現在値として別に出力します。

node scripts/fetch-ethereum-rpc-snapshot.mjs --fetch

scriptはHTTPS、credentialなし、query parameterなし、allowlist済みhostnameだけを受け付けます。環境変数でendpointを差し替える場合も同じ条件です。authorization header、wallet、private key、署名、transactionは扱いません。

field committed measurement
checkedAt 2026-08-30T18:46:35.912Z
network / chain ID Ethereum mainnet / 1
region JP
plan public unauthenticated; no account or API key
Node / viem Node 24 / viem 2.56.0
latency ranking 実施していない
fixed block 25,869,830 / 0x53d1…5294
historical probe block 16,000,000

tag support、archive depth、rate limit、cache、routing、planは変わります。後日の--fetchでarchive errorが消える、別errorになる、endpoint自体が終了する可能性があります。committed JSONは執筆時のraw observation、live fetchは現在の再確認として分けてください。

provider差を調べる順番

値が違うときは、次の順に狭めます。

  1. chain ID、contract address、ABI、function selector、argumentsを固定する
  2. 各responseのblock numberとhashを記録する
  3. latestを一度safeまたはfinalizedへ解決し、explicit blockで再実行する
  4. number取得後に別endpointで同じhashか確認する
  5. batch responseを配列位置ではなくIDで結合する
  6. Multicallのouter block、contractごとのsuccess、allowFailureを記録する
  7. historical failureのHTTP status、RPC code、message、plan、region、timestampを残す
  8. RPC node、indexer、cache、Explorer APIのどの層を読んだか分ける

latestでだけ違い、fixed blockで一致するならmoving headまたは近いreorgが第一候補です。fixed blockでも一方だけ古いcallに失敗するならstate historyまたはplan boundaryを調べます。同じblock hashでraw RPCは一致するのにExplorer集計だけ遅れるなら、indexer lagやcacheを別層として確認します。

blob dataのようにexecution stateとは別のretention境界を持つ証拠は、Fusaka・PeerDASとblob feeの固定block検証で扱っています。execution RPCがtransaction hashを返しても、別APIのhistorical dataが永久に残るとは限りません。

同じ考え方でOP StackのPortal stateやArbitrumのOutbox stateを確認する場合は、L2出金をprove・challenge・finalize・claimへ分ける手順を参照してください。ERC-20のraw balanceOfをdecimalsとwallet表示へ対応させる場合は、tokenが表示されない時の確認順へ進みます。latestのcontract値と過去receipt timestampを、同じ時点の保証として混ぜないことが重要です。

Ethereum RPC snapshot確認チェックリスト

  • chain ID、address、ABI、argumentsを固定した
  • pending / latest / safe / finalizedのどれを選んだか記録した
  • tagを一度numberとhashへ解決した
  • contract reads、raw storage、logsへ同じblockを渡した
  • number固定とhash / canonicality確認を分けた
  • EIP-1898 unsupported errorも保存した
  • sequential / batch / Multicallを同じblock条件で比較した
  • batch responseをIDで対応させた
  • Multicallへ入らないlogsとraw storageを別RPCで固定した
  • historical stateに必要なretention / archive / planを確認した
  • endpoint、region、plan、timestamp、HTTP status、RPC errorを残した
  • public endpointの一回の観測を永久仕様や性能順位にしていない

結論は、RPC snapshotは先にblock numberとhashを一度解決し、price、balance、allowance、raw storage、logsの全readを同じblockへ対応させることです。sequential、batch、Multicallはrequest形状を選ぶ道具で、snapshot identityやarchive availabilityは別に設計します。

この記事にスポンサー、affiliate、wallet接続、署名要求、transaction送信、資産操作のCTAはありません。将来RPC、node hosting、monitoring、data infrastructureの広告を置く場合も広告であることを明示し、測定method、raw result、provider observation、technical conclusionから分離します。

endpoint差の原因が429、timeout、JSON-RPC code、node policy、transaction validation、EVMのどこにあるかを先に分ける場合は、Ethereum RPCエラーの層別診断でraw responseとviemのcause chainを照合してください。

確認した一次情報