Indexer: per-block Multicall3 batching for warbow_score + podium_live ingest RPC #356
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:open_issues
agents
agent:security_audit
agent:verify
area
timecurve
arena
arena-v2
audit
block:feature_review
block:security
bug
contracts
design
devops
docs
documentation
enhancement
epic
feature
frontend
indexer
low priority
megaeth
operations
ops
performance
QA
qa checklist
referrals
removal
security
smart-contracts
tech-debt
testing
time-arena
timecurve
visual-qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
code/yieldomega#356
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Batch
warbow_scoreandarena_podium_livepost-logeth_callside-effects during block ingestion into one Multicall3aggregate3per block (atBlockId::Number(block)), mirroring the chain-timer head poller (#307). Today eachBuy/ WarBow log triggers ~9 sequentialeth_calls; per-block planning + dedupe should collapse that to ~1 counted RPC per block (plus unchanged single-call paths where batching has no win).Parent / related: #306 (RPC metrics) · #307 (chain-timer Multicall3) · #140 (transactional block ingest) · #317 (ingest side-effects fail-closed) · #273 (live podiums) · #254 (WarBow BP snapshots).
Current codebase
Ingestion loop (
indexer/src/ingestion.rs)eth_getBlockByNumber+eth_getLogsper block.INV-INDEXER-140).TimeArenalog, afterpersist_decoded_log_conn, ingestion runs two independent RPC paths:warbow_score::snapshot_warbow_players_after_log—podiumEpoch(3)+battlePoints(player)per affected player (2+eth_calls per log).arena_podium_live::snapshot_live_podium_after_log— sequential epoch lookups +podium(cat)snapshots (up to 7eth_calls onArenaBuy).WarBow snapshots (
indexer/src/warbow_score.rs)eth_call_u256records oneRpcMethod::EthCallper call withRpcCaller::WarbowScore.ArenaBuy, steal/revenge/flag events (not onArenaWarbowEpochScore— already emitted on-chain).encode_u8_call/encode_address_callagainstTimeArenaatd.block_number.Live podium projections (
indexer/src/arena_podium_live.rs)snapshot_live_podium_after_logbranches on event type;ArenaBuyis the expensive path:lastBuyEpoch→podium(0)→podiumEpoch(1,2)+podium(1,2)→podiumEpoch(3)→ DB rollup for WarBow top-3.podium_at_blockrecordsRpcCaller::PodiumLivepereth_call.podiumEpoch(3)is fetched twice on buys (warbow path + podium path).Existing Multicall3 transport (
indexer/src/multicall.rs)MulticallBatchbuilder with dedupe on(target, callData).aggregate3_at_block—allowFailure: falsefor every sub-call; records one logicaleth_callinrpc_metrics.chain_timer::poll_once_multicall(#307); falls back to sequential reads on failure.RPC metrics (
indexer/src/rpc_metrics.rs)by_caller/by_methodare raw call counts in a 5-minute window, not per logical update.warbow_score+podium_live~1,100eth_calls / 5 min vs ~180/min forchain_timer.Policy (
docs/indexer/design.md,INV-INDEXER-317-INGEST-SIDE-EFFECTS)Why this is needed
Buylogs per block; each buy currently adds ~9 sequentialeth_calls on top of per-blockgetBlock+getLogs. This dominates indexer RPC during active play (seedocs/indexer/benchmarks/rpc-benchmark-fullstack-swarm-20260628T052907Z.json).podiumEpoch/ epoch head reads are repeated across logs and across warbow vs podium paths;MulticallBatchdedupe eliminates redundant sub-calls.peak_calls_10s; ingest-sideeth_callbursts scale with tx count, not block time.Constraints and guardrails
INV-INDEXER-140INV-INDEXER-317-INGEST-SIDE-EFFECTSINV-INDEXER-273-*GET /v1/arena/podiumslive predictions must match pre-change behavior;podium_predictionsemantics unchanged.INV-INDEXER-307-MULTICALL-BATCHmulticall.rs;allowFailure: falseon all sub-calls — historicaleth_calls at log block should not revert on healthy chain state; a revert indicates indexer bug or corrupt log context and must fail the block.INV-INDEXER-273-CHAIN-TIMER-SELECTORSpodiumEpoch(uint256)/podiumDeadline(uint256)array getters (notuint8overloads).scripts/lib/anvil_multicall3.sh(already in stack). Production MegaETH ships canonical0xcA11…CA11.aggregate3fails (missing Multicall3, transport error), fall back to existing sequential path for that block (same aschain_timerpoll_once_sequential) — preserves liveness; log atdebug/warn.Relevant files
indexer/src/ingestion.rsindexer/src/warbow_score.rsindexer/src/arena_podium_live.rsindexer/src/multicall.rspush_address_arg; possibly export decode helpersindexer/src/chain_timer.rsencode_u8_call,decode_podium_return, selector constantsindexer/src/rpc_metrics.rseth_callperaggregate3(considerRpcCaller::Ingestionvs split warbow/podium — document choice)indexer/src/rpc_http.rsindexer/tests/integration_stage2.rsscripts/verify-podium-live-anvil.shscripts/verify-wallet-profile-anvil.shscripts/benchmark-indexer-rpc-anvil.shdocs/indexer/design.mddocs/testing/invariants-and-business-logic.mdINV-INDEXER-329-*(or next free ID)docs/indexer/rpc-load-benchmark.mdRecommended solution direction
1. New module:
indexer/src/arena_ingest_rpc.rs(name TBD)plan_block_rpc_batch(block, arena_logs: &[DecodedLog]) -> BlockRpcPlanTimeArenalog, append required sub-calls to aMulticallBatch(dedupe handles sharedpodiumEpochreads).battlePoints(player),podium(cat, epoch)) to a stable result index in the batch.execute_block_rpc_batch(providers, block, batch, metrics) -> Vec<Bytes>aggregate3_at_blockwithallowFailure: false.eth_calls — acceptable degradation).2. Per-event read sets (planning)
ArenaBuylastBuyEpoch,podiumEpoch(0..3),podium(0..2)at block,battlePoints(buyer)ArenaWarbowSteal/Revenge/FlagClaimed/GuardpodiumEpoch(3),battlePointsper playerArenaPodiumEpochRolledpodium(category)ArenaLastBuyEpochStartedpodium(0)ArenaWarbowEpochScore3. Ingestion integration
Preferred: plan from decoded logs before RPC, execute one multicall, then run existing insert helpers with pre-fetched bytes (minimize behavior drift).
4.
MulticallBatch::push_address_argMirror
warbow_score::encode_address_callforbattlePoints(address).5. Metrics
eth_callper successfulaggregate3(match #307).ingest_multicall_subcalls=N deduped_from=M.Acceptance criteria
ArenaBuylogs, ingest performs at most one Multicall3aggregate3eth_callfor all warbow + podium live reads at that block (excluding fallback path).idx_warbow_epoch_scoreandidx_arena_podium_liverows for a given test fixture are byte-identical to pre-change sequential ingest (same players, scores, epochs, block tags).GET /v1/arena/podiumsandGET /v1/arena/wallet/{addr}/statsresponses unchanged forverify-podium-live-anvil.shandverify-wallet-profile-anvil.sh.allowFailure: falseon all sub-calls; any sub-call failure fails the batch and rolls back the block transaction.idx_arena_podium_live/ warbow tables (integration_stage2rollback assertions pass).eth_call:warbow_score+eth_call:podium_liveper-minute counts drop ≥80% vs pre-change baseline on same harness (benchmark-indexer-rpc-anvil.sh, active_arena scenario).invariants-and-business-logic.md+design.md.Test plan — functional paths
ArenaBuycargo testinarena_ingest_rpc— assert sub-call count + dedupe ofpodiumEpoch(3)battlePoints+podiumEpoch(3)ArenaPodiumEpochRolledpodium(cat)sub-callArenaLastBuyEpochStartedpodium(0)ArenaWarbowEpochScoreintegration_stage2.rs::postgres_stage2_persist_all_events_and_rollback_afterintegration_stage2.rs::arena_podiums_live_predictions_smokebash scripts/verify-podium-live-anvil.shbash scripts/verify-wallet-profile-anvil.shbash scripts/verify-indexer-rpc-metrics.shBENCHMARK_SCENARIO_SEC=120 bash scripts/benchmark-indexer-rpc-anvil.sh— compare active_arenaeth_calltotalsSTART_BOT_SWARM=1 bash scripts/start-qa-local-full-stack.sh --live-sale+ sample/v1/status/opsrpc_metricscd indexer && cargo clippy --all-targets -- -D warnings && cargo testTest plan — attack, hack, and abuse vectors
idx_arena_podium_liverows (INV-INDEXER-317). Simulate with mock provider or timeout.Buylogs in one block to amplify RPCeth_callcount.rollback_afterclears derived tables; re-ingest reproduces state (integration_stage2reorg test).verify-podium-live-anvil.sh; batch must usepodiumEpoch(uint256)only.BlockId::Number(d.block_number); integration fixture assertsblock_numberon stored rows matches log.log_indexlog_index * 10 + ischeme unchanged; integration asserts no PK violations under batch path.ON CONFLICT DO NOTHINGon warbow inserts preserved.Verification criteria (merge gate)
cd indexer && cargo clippy --all-targets -- -D warnings && cargo testbash scripts/verify-podium-live-anvil.shbash scripts/verify-wallet-profile-anvil.shbash scripts/verify-indexer-rpc-metrics.shBENCHMARK_SCENARIO_SEC=120 bash scripts/benchmark-indexer-rpc-anvil.shwarbow_score+podium_livereduction in MRdesign.md,invariants-and-business-logic.md,rpc-load-benchmark.mdOut of scope
chain_timerfurther (done in #307).eth_getBlockByNumber/eth_getLogsacross blocks.allowFailure: trueper sub-call (explicitly not desired — reverts should not occur on valid historical reads).Suggested labels
indexer,performance,arena-v2Suggested weight
5
marked as related to #306
marked as related to #307
marked as related to #140
marked as related to #317
marked as related to #273
marked as related to #254
mentioned in merge request !530
mentioned in commit
e1be645b69Verification — GitLab #356 (per-block Multicall3 ingest batching)
Verified on
mainat 2026-06-28. Implementation shipped inarena_ingest_rpc.rs; no repo changes required from this verify pass.Acceptance criteria
cargo test arena_ingest_rpc— singleArenaBuyplans deduped batch; active-arena benchmark last sample showseth_call:ingestion(not per-logwarbow_score/podium_live)integration_stage2.rs(postgres_stage2_persist_all_events_and_rollback_after+arena_podiums_live_predictions_smoke) — 7/7 integration tests greenbash scripts/verify-podium-live-anvil.sh— OK (read_block=25,live_rows=12);bash scripts/verify-wallet-profile-anvil.sh— OKmulticall::tests::decode_aggregate3_results_rejects_failed_subcall; ingest usesallowFailure: falseviaaggregate3_at_block; rollback clears derived tables inintegration_stage2INV-INDEXER-356-FALLBACK—execute_sequential_batchinarena_ingest_rpc.rs; manual Anvil-without-Multicall3 smoke confirmed chain_timer + ingest sequentialeth_calls (noaggregate3); DB writers identical to batch path (parity tests AC-2)postgres_stage2_persist_all_events_and_rollback_after— reorg rollback clearsidx_arena_podium_live/ warbow tablesBENCHMARK_SCENARIO_SEC=120 bash scripts/benchmark-indexer-rpc-anvil.sh— active_arena last sample:eth_call:warbow_score+eth_call:podium_live= 0 vs pre-change baseline20260615T021945Z297 (100% reduction, ≥80% target)INV-INDEXER-356-*indocs/testing/invariants-and-business-logic.md§356;docs/indexer/design.md§317;docs/indexer/rpc-load-benchmark.mdpost-#356 tableTest plan
cd indexer && cargo test arena_ingest_rpc— 7 unit tests (buy dedupe, multi-buy, steal, epoch rolled, last-buy, warbow score no-RPC)cargo test --test integration_stage2 postgres_stage2_persist_all_events_and_rollback_afterarena_podiums_live_predictions_smoke(insideintegration_stage2+verify-podium-live-anvil.sh)bash scripts/verify-podium-live-anvil.shbash scripts/verify-wallet-profile-anvil.shbash scripts/verify-indexer-rpc-metrics.sh— schema=2.20.0,total_calls=57BENCHMARK_SCENARIO_SEC=120 bash scripts/benchmark-indexer-rpc-anvil.sh/v1/status/opsrpc_metricsunder sustained buys — same signal for ingest RPC attributioncd indexer && cargo clippy --all-targets -- -D warnings && cargo test— 89 unit + 7 integration testsMerge gate
cargo clippy+cargo testverify-podium-live-anvil.shverify-wallet-profile-anvil.shverify-indexer-rpc-metrics.shbenchmark-indexer-rpc-anvil.sh(120s)INV-INDEXER-356-*, design.md, rpc-load-benchmark.mdAttack / abuse vectors (spot-check)
eth_call:ingestionper block under N buys — confirmed in benchmark (legacy callers → 0).integration_stage2.podiumEpoch(uint256)selectors — unit tests + podium-live E2E.Closing — all automated acceptance items pass; T-13 deferred to operator manual QA per issue test plan label.