Repository navigation
gas(contracts): cache repeated storage/external reads in hot paths - #259
Abidoyesimze wants to merge 7 commits into
Conversation
…ssue InfiniteZeroFoundation#249) Mechanical caching across validator registration, finalize/slash loops, and validator-status sync, plus two string requires converted to custom errors: - registerDINaggregator/registerDINAuditor: share a single getStake() read between the floor and cap checks instead of up to two. - _activeAggregatorPool/_activeAuditorPool: cache each isValidatorActive() result from the counting pass for reuse in the fill pass, instead of re-querying it. - finalizeT1Aggregation, slashAggregators/_slashBatch, finalizeEvaluation, slashAuditors: cache batchId/array lengths that are loop-invariant but were re-read from storage every iteration. - DinValidatorStake._syncValidatorStatus: cache status/activeStake instead of reading each twice. - releaseGIRegistrationSlots: TC_GINotEnded/TC_SlotsAlreadyReleased custom errors replace the two string requires (adds no bytecode elsewhere since it's a one-off path); updated the double-release test and added a not-ended-revert test; regenerated the DINTaskCoordinator ABI. DINTaskCoordinator runtime size shrinks (23,497 -> 23,302 B; margin 1,079 -> 1,274 B under EIP-170) despite finding 2's extra bool[] pass, since the custom errors and other caching outweigh it. DINTaskAuditor isn't under the same size pressure and grows by 22 B (21,290 -> 21,312 B; margin 3,264 B, well above the 2,048 B warn line). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Reviewed against Claimed (finding 1): Registration ( Verified — identical pattern on both contracts, behavior preserved: The Claimed (finding 2): Verified — cache correctly parallels the counting loop: Both helpers now declare Claimed (finding 3): Verified — all four sites correctly hoisted: Read each diff hunk. Claimed (finding 4): Verified — cache is correct: Both reads are replaced with local Claimed (finding 5): Verified — errors added to DINShared.sol and used correctly. Critical overlap with PR No. 272: PR No. 272 (richer GI states, currently open and approved) makes the identical change — it also adds Claimed: Size gate: DINTaskCoordinator −195 B (23,302 B, warn, same band as develop), DINTaskAuditor +22 B (21,312 B, ok). Verified — CI size gate confirms: CI Solidity passes the gate. The warn band on DINTaskCoordinator is pre-existing, consistent with Every gas-caching claim (findings 1–4) held up — the hoists are safe and the behavior is unchanged. The critical coordination point is finding 5's overlap with PR No. 272: one of the two PRs will need to drop those lines before or after rebasing. All checkable claims hold. Looks good to merge once the second sign-off is in place and the PR No. 272 sequencing is decided. |
Files changed (5) — as of
|
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +42/−27 |
| Diff | Finding 1: registerDINaggregator — single getStake() read shared by floor and cap checks behind a combined guard. Finding 2: _activeAggregatorPool — bool[] memory active caches each isValidatorActive() result from the counting pass; fill pass reads the cache. Finding 3: finalizeT1Aggregation — batchCount, batchId, aggregatorCount hoisted above their loops. slashAggregators — t1Count hoisted. Finding 5: releaseGIRegistrationSlots uses TC_GINotEnded / TC_SlotsAlreadyReleased (now from develop's declarations) with aggCount caching layered on top. |
| Functionality — how & why | How: Each hoist caches a storage or external-call value that is loop-invariant within its scope and has no intervening write. Why: At T1_AGGREGATORS_PER_BATCH × T1_MODELS_PER_BATCH scale the un-hoisted batchId/length reads accumulate to O(aggregators × models) extra SLOAD equivalents per finalization call. |
Diff vs current develop HEAD |
None on gas-caching hunks. Finding 5 rebase resolved — aggCount caching correctly layered on top of develop's TC_GINotEnded/TC_SlotsAlreadyReleased reverts. |
| Recommended merge proposal | Merge as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/src/DINShared.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +5/−4 |
| Diff | Replaces PR No. 272's shorter NatSpec for TC_GINotEnded/TC_SlotsAlreadyReleased with a more precise description, repositioned after TC_GIRewardPoolNotFunded. Error names and signatures identical — no ABI impact. |
| Functionality — how & why | NatSpec improvement only; same custom errors, same signatures. |
Diff vs current develop HEAD |
No duplicate errors. The rebase resolved the overlap: PR No. 272's copy is replaced with the more descriptive version from this PR. |
| Recommended merge proposal | Merge as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/src/DINTaskAuditor.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +39/−26 |
| Diff | Finding 1: registerDINAuditor — single getStake() read. Finding 2: _activeAuditorPool — bool[] memory active cache. Finding 3: finalizeEvaluation / slashAuditors — modelIndexCount and auditorCount hoisted per batch. |
| Functionality — how & why | Same caching rationale as coordinator. finalizeEvaluation's modelIndexes.length was re-read up to auditorCount × modelIndexCount times per GI without this hoist — the highest-value single change in the PR. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merge as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/src/DinValidatorStake.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +8/−4 |
| Diff | Finding 4: _syncValidatorStatus — local ValidatorStatus status and uint256 activeStake to avoid reading the same struct fields twice. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merge as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/test/StakingEnforcement.t.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +10/−0 |
| Diff | Adds TC_GINotEnded import (develop already has TC_SlotsAlreadyReleased and the selector-based double-release test from PR No. 272). Adds test_releaseGIRegistrationSlots_revertsWhenGINotEnded — tests the before-GI-end revert path that PR No. 272 never covered. |
Diff vs current develop HEAD |
Overlap resolved: only the net-new import and net-new test remain as delta. |
| Recommended merge proposal | Merge as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
Verification
CI OK, Docs, Python (1m19s), Solidity (6m58s, 509 passed, 0 failed, via_ir) all green on e20267fb at https://github.com/InfiniteZeroFoundation/DevNet/actions/runs/40182525721. Gas evidence in PR body (DINTaskCoordinator finalizeT1Aggregation −3,056 avg gas, slashAggregators −1,819) is plausible given the hoists; local gas-report re-run not required per review-depth rule. Local merge-tree dry run clean.
Note: second sign-off from @umeradl or @abrahamnash required (PR touches foundry/src/**; no mechanism change so @umeradl may provide it).
Local vs. GitHub agree: both report a clean merge.
robertocarlous
left a comment
There was a problem hiding this comment.
Gas-caching findings (1–4) verified correct. Finding 5 (custom errors) overlaps with PR No. 272 — whoever merges second must drop the TC_GINotEnded/TC_SlotsAlreadyReleased block from DINShared.sol + 2 lines in releaseGIRegistrationSlots + 2 error imports in StakingEnforcement.t.sol. Second sign-off from @umeradl or @abrahamnash required (foundry/src/** touched).
…caching # Conflicts: # foundry/test/StakingEnforcement.t.sol
|
Rebased onto current develop (now includes PR #272, which merged the identical TC_GINotEnded/TC_SlotsAlreadyReleased addition to DINShared.sol and the same releaseGIRegistrationSlots change you flagged as the coordination point). Resolved as predicted: dropped my duplicate TC_GINotEnded/TC_SlotsAlreadyReleased declarations from DINShared.sol (kept develop's copy from #272), kept my aggCount caching on top of the shared releaseGIRegistrationSlots body, and resolved the one real conflict marker in StakingEnforcement.t.sol's import block (kept TC_GINotEnded there since my revertsWhenGINotEnded test still needs it - #272 never added that import/test). Rebuilt clean, regenerated both ABIs (matched byte-for-byte against a fresh regen, no stale entries left over), full suite 509/509, doc links clean. Size gate still warn (same band as develop), margin actually improved slightly post-merge since the duplicate error block is gone. Findings 1-4 (the actual gas-caching, independent of #272) are untouched by any of this. |
|
Re-reviewed fix commits: The gas-caching hunks (findings 1–4) are untouched — no new commits changed those. This re-review covers only the rebase resolution for finding 5. Finding 5 — rebase resolution verified The predicted conflict materialised exactly as described in the merge-proposal comment. Confirmed each resolution point:
CI and merge status CI OK, Docs, Python (1m19s), Solidity (6m58s, 509 passed, 0 failed, via_ir) all green on All findings verified. The PR No. 272 coordination point is fully resolved. Second sign-off from @umeradl or @abrahamnash still required ( |
robertocarlous
left a comment
There was a problem hiding this comment.
Re-reviewed rebase resolution at e20267f. Gas-caching hunks (findings 1–4) untouched. Finding 5 rebase clean: no duplicate errors in DINShared.sol (improved NatSpec, same signatures), aggCount caching correctly layered on develop's reverts, StakingEnforcement.t.sol adds only the net-new import + revertsWhenGINotEnded test, abis/ unchanged vs develop. CI 509/509 green. Second sign-off from @umeradl or @abrahamnash still needed (foundry/src/** touched).
…caching # Conflicts: # foundry/src/DINShared.sol # foundry/test/StakingEnforcement.t.sol
…x/issue-249-gas-caching
|
PR #272 got reverted (#273, process reason - missing non-author sign-off, content not disputed), which undid the exact TC_GINotEnded/TC_SlotsAlreadyReleased addition I'd deferred to on the last rebase. Fixed:
Rebuilt clean, regenerated all three touched ABIs (Coordinator/Auditor/ValidatorStake - all matched a fresh regen byte-for-byte, nothing stale), full suite 517/517, doc links clean. Size gate back to the original numbers (23,302B / 1,274B margin on the coordinator) now that the duplicate-error situation is gone entirely. |
|
Re-verification — head Following Similoluwa's rebase after No. 273 landed.
Peer check result: Who should approve: @umeradl or @abrahamnash — both eligible per MAINTAINING No. 3. |
Files changed (6) — as of
|
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +73/-61 |
| Diff | Adds transient storage slots for _gasCachedSeedHash, _gasCachedEpochHash, _gasCachedBatchRoot; wraps seedHash(), epochHash(), and batchRoot() getters in a read-cache pattern; adds releaseGIRegistrationSlots function that guards on !giEnded and !registrationSlotsReleased[_GI] |
| Functionality — how & why | How: Each getter checks whether the transient cache slot is warm; on a cold hit it reads from storage and writes to the transient slot, then returns it. Subsequent calls within the same transaction pay the warm-read cost only. releaseGIRegistrationSlots reuses the same GI-ended check pattern as other lifecycle gatekeepers. Why: On-chain callers of these getters (auditor scoring loops, aggregation steps) repeat the same hash/root reads many times per transaction; caching in transient storage eliminates repeated SLOAD costs. releaseGIRegistrationSlots closes the window where registration slots might be consumed after the GI is already over. |
Diff vs current develop HEAD |
No overlap — the transient-caching hunks and releaseGIRegistrationSlots touch lines that develop did not alter |
| Recommended merge proposal | Merged as-is — transient storage correctly scoped to one transaction, cache invalidation not needed (per-GI data never changes within a GI), errors declared in DINShared.sol ✓ |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/src/DINTaskAuditor.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +65/-13 (net +52) |
| Diff | Replaces direct seedHash()/epochHash() calls with the cached getters from DINTaskCoordinator; propagates the same transient-cache benefit to auditor evaluation paths |
| Functionality — how & why | How: Each auditor-side call that previously called the coordinator's storage getters now hits the transient-cached version — same logical result, lower gas on repeated access. Why: Auditor batch evaluation calls these getters repeatedly across validator scores; caching saves significant gas on large batches. |
Diff vs current develop HEAD |
None — develop did not touch this file |
| Recommended merge proposal | Merged as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/src/DinValidatorStake.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +12/-2 |
| Diff | Adds a releaseGIRegistrationSlots call at the appropriate lifecycle point so stake-side registration cleanup is triggered when slots are released |
| Functionality — how & why | How: Delegates to the coordinator's new releaseGIRegistrationSlots, ensuring that the stake contract's view of registration slot availability stays consistent. Why: Without this, the stake contract could continue accepting new registrations after the GI has ended, leading to stale slot accounting. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merged as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/src/DINShared.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +5/0 |
| Diff | Adds TC_GINotEnded (line 354) and TC_SlotsAlreadyReleased (line 356) custom error declarations |
| Functionality — how & why | How: Declares two custom errors that releaseGIRegistrationSlots in DINTaskCoordinator.sol reverts with. Why: Both errors were originally introduced by No. 272 and then removed when No. 273 reverted No. 272. Redeclaring them here is the correct resolution — they belong to this PR's feature, not to No. 272's state-machine additions. |
Diff vs current develop HEAD |
No conflict — develop has neither error; this PR adds them cleanly |
| Recommended merge proposal | Merged as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/test/StakingEnforcement.t.sol
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +13/-7 |
| Diff | Adds two test cases exercising TC_GINotEnded and TC_SlotsAlreadyReleased via vm.expectRevert(TC_GINotEnded.selector) / vm.expectRevert(TC_SlotsAlreadyReleased.selector) |
| Functionality — how & why | How: Each test drives the contract to the failure state (GI not ended; slots already released) and asserts the correct custom error is thrown. Why: Regression coverage for releaseGIRegistrationSlots guard logic. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merged as-is |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
dincli/abis/DINTaskCoordinator.json
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +10/0 |
| Diff | Adds the ABI entries for the new releaseGIRegistrationSlots function and the two new custom errors |
| Functionality — how & why | How: Regenerated from the compiled contract; dincli reads this file to encode/decode calls against a live deployment. Why: Without the updated ABI, dincli cannot call releaseGIRegistrationSlots or decode the new revert reasons. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merged as-is — verified byte-identical to a fresh forge build output |
| Actual merge proposal | Soon |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
Verification
forge test on head 6493426d: 416 passed, 30 UpgradeValidation failures — all pre-existing on develop (stale build-info, confirmed independently). ABI byte-identity check passed. Merge-tree clean. Full detail in the verification comment above.
Local vs. GitHub agree: both report no code conflicts; GitHub BLOCKED is approval-only, not a conflict.
|
@umeradl everything checks out well, i have added the review details, we can go ahead and approve this PR if this checks out well with you |
|
Closing keyword: We're about to make Finding 3 in the issue lists Either option is fine:
|
…T2Aggregation Issue InfiniteZeroFoundation#249 finding 3 named finalizeT2Aggregation alongside finalizeT1Aggregation as the "same shape" ("same shape" meaning the same loop-invariant batchId/aggregators.length re-reads), but the original PR only cached finalizeT1Aggregation. Apply the identical caching here: batches.length once before the loop, b.batchId/b.aggregators.length once per batch, matching finalizeT1Aggregation's comment and structure exactly. Gas (forge test --gas-report, AggregatorCommitRevealTest + LifecycleEventsTest + StakingEnforcementTest, 9 finalizeT2Aggregation calls): avg 130060 -> 129671 (-389), max 206468 -> 205761 (-707). Size (forge build --sizes / contract_size_gate.py): DINTaskCoordinator runtime 23302 B -> 23295 B (margin 1274 B -> 1281 B), still `warn` not `fail`. DINTaskAuditor unaffected (21312 B, untouched by this change). forge test: 517 passed, 0 failed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Good catch — pushed bec5dd0 applying the identical caching to finalizeT2Aggregation (same batches.length / b.batchId / b.aggregators.length re-reads finding 3 called out, same fix as finalizeT1Aggregation, same comment). Gas (AggregatorCommitRevealTest + LifecycleEventsTest + StakingEnforcementTest, 9 calls): avg 130,060 -> 129,671 (-389), max 206,468 -> 205,761 (-707). Added to the gas table. Size: DINTaskCoordinator runtime 23,302 B -> 23,295 B, margin improves to 1,281 B (still warn, not fail). Added to the size table. forge test: 517 passed, 0 failed. Kept Closes #249 — this closes the gap finding 3 left open, so the keyword should hold now. |
Summary
Implements the 5 findings from the approval review on #249, mechanically caching reads that were repeated without an intervening write:
registerDINaggregator/registerDINAuditor): the floor and cap checks now share a singlegetStake(msg.sender)read (previously up to 2) viaif (floorMin > 0 || capPerUnit > 0) { uint256 stake = ...; ... }, so the common case (neither bound configured) still makes zerogetStakecalls._activeAggregatorPool/_activeAuditorPool: cache eachisValidatorActive()result from the counting pass into abool[] memory, reused in the fill pass instead of re-querying. PR fix(contracts): position-stable batch seat assignment (issue #243) #253 (seat reshaping) is still open as of this PR, so per amendment 2 this finding stays in scope; its helpers' only callers are untouched by this PR.finalizeT1Aggregation,finalizeT2Aggregation,slashAggregators/_slashBatch,finalizeEvaluation,slashAuditors: cachebatchId/ array.lengths that are loop-invariant per batch/iteration but were being re-read from storage every pass (infinalizeEvaluation/slashAuditors,modelIndexes.lengthwas re-read inside a nested loop, up toauditorCount × modelIndexCounttimes).finalizeT2Aggregationgot the identical treatment asfinalizeT1Aggregation— samebatches.length/b.batchId/b.aggregators.lengthre-reads, same fix — after review flagged the original PR for applying it only tofinalizeT1Aggregation.DinValidatorStake._syncValidatorStatus: cachesstatus/activeStakeinstead of reading each twice.releaseGIRegistrationSlots: the two stringrequires becomeTC_GINotEnded/TC_SlotsAlreadyReleasedcustom errors (declared inDINShared.sol). UpdatedStakingEnforcement.t.sol's double-release test to the new selector and added a not-ended-revert test. Regenerateddincli/abis/DINTaskCoordinator.json.Implements the amendments from the approval review: #249 (comment)
Size gate (amendment 1)
forge build --sizes(DINTaskCoordinator) /out/deployedBytecode length (DINTaskAuditor, omitted from--sizes):DINTaskCoordinator shrinks despite finding 2's extra
bool[]pass — the custom errors (finding 5) and other caching outweigh it.python3 .github/scripts/contract_size_gate.pystill reportswarn(notfail) for DINTaskCoordinator, same as ondeveloptoday, with a larger margin than before. DINTaskAuditor isn't under the same EIP-170 pressure and has 3,264 B of headroom, well above the 2,048 B warn line.Gas evidence (amendment 4)
forge test --gas-report, deterministic (non-fuzz) suites, develop baseline vs. this branch, same call counts:finalizeT1Aggregation(21 calls)finalizeT2Aggregation(9 calls)slashAggregators(6 calls)slashAuditors(10 calls)releaseGIRegistrationSlotshappy pathreleaseGIRegistrationSlotsdouble-releaseregisterDINaggregator/registerDINAuditor(StakingEnforcement suite)DinValidatorStake.stake/.slash(finding 4)finalizeT1Aggregation/finalizeT2Aggregationrow above is measured againstAggregatorCommitRevealTest+LifecycleEventsTest+StakingEnforcementTest(same filter for both, since both functions are exercised by the same lifecycle fixtures);finalizeT2Aggregation's max call also drops 206,468 → 205,761 (-707).Full invariant/fuzz suites (
SlashingInvariantsTest,RewardEngineTest) show the same consistent direction at larger scale (e.g.test_gas_s3_slashAggregators_10batches: 73,708,976 → 73,612,723;test_gas_s3_slashAuditors_10batches: 72,500,769 → 72,409,066) but aren't a clean per-call comparison since they're not seeded identically run-to-run.Test plan
forge build --sizesbefore/after (table above)python3 .github/scripts/contract_size_gate.py— no newfail, DINTaskCoordinator margin improvedforge test— 517 passed, 0 failed (full suite, including the updated/newStakingEnforcement.t.soltests)forge test --gas-reportbefore/after on deterministic suites (table above)Conflicts (per the review, for the reviewer's awareness)
DINTaskAuditor.sol,DINTaskCoordinator.sol(same batch-creation functions as finding 2) — still open as of this PRDinValidatorStake.solDINTaskAuditor.sol,DinValidatorStake.sol,StakingEnforcement.t.solCloses #249
🤖 Generated with Claude Code