fix(task-auditor): bind the auditor and slot into the audit commit hash (#192) - #212
Conversation
…sh (InfiniteZeroFoundation#192) revealAuditScore checked keccak256(abi.encodePacked(score, vote, salt)), which carries no address, GI, batch or model. An assigned auditor could copy a peer's public commit hash during LMSevaluationStarted, wait for the peer's reveal, then reveal the same (score, vote, salt) and earn a quorum vote and reward weight without evaluating anything. The hash is now keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex)), matching the aggregation side (InfiniteZeroFoundation#156 M-1). Function signatures and the bundled ABI are unchanged. - dincli: _audit_commit_hash (eth_abi.encode + keccak, modelled on _agg_commit_hash) used by `auditor lms-evaluation evaluate --submit`; reveal is unchanged. Golden-vector tests pinned via cast. - foundry tests: one shared auditCommitHash helper (test/utils/AuditCommitHash.sol); all 25 commitAuditScore sites in 11 files now hash per auditor instead of reusing one hash across a loop. - regression tests: copied hash + copied reveal reverts TA_RevealHashMismatch; a hash built for another model, batch or GI doesn't reveal; the honest path still reveals and closes. - docs: DINTaskAuditor.md §7, §13 No. 2 (fixed) and change log, DINShared.md, security review. DINTaskAuditor runtime 22,567 -> 22,589 B (margin 1,987 B). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Reviewed against Claimed: Verified:
Claimed: the dincli hash Verified, exact, plus break-then-fix:
Claimed: all 25 Verified:
Claimed: the new regression tests catch the copy attack and cross-slot reuse. Verified, break-then-fix: In both Claimed: Verified, exact: I ran PR No. 211's Claimed: 479 forge tests pass, and pytest passes (313 without torch). Verified: Beyond the PR — combined with PR No. 211: both PRs touch Not independently re-verified: Minor, optional: Every checkable claim held up. The contract fix is minimal and mirrors the coordinator's sender-bound hash. The dincli hash matches the contract byte-for-byte against |
Files changed (18) — as of
|
| Field | Value |
|---|---|
| Change | Modified |
| Lines | +14/-5 |
| Diff (what exactly is in this PR) | revealAuditScore's expectedHash changes from keccak256(abi.encodePacked(score, vote, salt)) to keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex)). The formula is updated in the state comment, in the commitAuditScore @dev, and in its @param. |
| Functionality — how & why | How: at reveal, the contract rebuilds the hash from the revealed (score, vote, salt) plus the caller's own address and the slot it is revealing for. It then compares that against auditScoreCommits[gi][batchId][msg.sender][modelIndex]. A hash copied from a peer was built with the peer's address, so it can't match under the copier's msg.sender. A hash built for another (gi, batchId, modelIndex) can't match either. Why: the old hash carried no identity. An assigned auditor could commit a peer's public hash, wait for the peer's reveal, and replay it, earning a quorum vote and auditorGIWeight reward weight without evaluating anything (issue No. 192, the auditor-side residual of the M-1 finding). |
Diff vs current develop HEAD |
None: untouched since the merge-base. |
| Recommended merge proposal | Merge as-is. +22 B runtime, giving 22,589 B and a 1,987 B margin (gate warning band, above the 1,024 B fail line). Break-then-fix: dropping the msg.sender + modelIndex binding (contract and helper) fails exactly the copy-attack and slot tests; the honest-path test still passes. |
| Actual merge proposal | Applied as-is, byte-identical to PR head. Size gate: 22,589 B runtime (1,987 B margin, warning band). dump-abi --official from the merged build leaves dincli/abis/DINTaskAuditor.json unchanged. Evidence on the merged tree (develop @ 420d48d + this PR): forge clean && forge build (via_ir) clean; size gate exit 0; forge test 484 passed / 0 failed / 0 skipped; pytest -m "not integration" 389 passed / 0 failed. Uncommitted in the develop cwd, not committed or pushed. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
dincli/cli/auditor.py, tests/test_auditor_commit_hash.py
| Field | Value |
|---|---|
| Change | Modified / New |
| Lines | +15/-2, +51/-0 |
| Diff (what exactly is in this PR) | Adds _audit_commit_hash(score, vote, salt, sender, gi, batch_id, model_index), which uses eth_abi.encode + Web3.keccak, and uses it in evaluate --submit in place of Web3.solidity_keccak. The new test file pins two cast-computed golden vectors and checks that the hash varies with the sender and with each slot field. |
| Functionality — how & why | How: evaluate --submit hashes with the same account.address, curr_GI, batch_id, model_index it then passes to commitAuditScore. It still caches (score, vote, salt) locally. reveal_lms sends those cached values unchanged and lets the contract recompute the hash. Why: with the contract change alone, every dincli reveal would revert TA_RevealHashMismatch, and the auditor would be S1-slashed for a missed vote. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merge as-is. Golden vectors re-derived with cast, identical. Swapping gi/batch_id, or switching to packed encoding, fails both golden-vector tests. Full pytest with torch: 389 passed. |
| Actual merge proposal | Both applied as-is (test file copied), byte-identical to PR head. Evidence on the merged tree (develop @ 420d48d + this PR): forge clean && forge build (via_ir) clean; size gate exit 0; forge test 484 passed / 0 failed / 0 skipped; pytest -m "not integration" 389 passed / 0 failed. Uncommitted in the develop cwd, not committed or pushed. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/test/utils/AuditCommitHash.sol, foundry/test/AuditorCommitReveal.t.sol
| Field | Value |
|---|---|
| Change | New / Modified |
| Lines | +19/-0, +85/-4 |
| Diff (what exactly is in this PR) | A new free function auditCommitHash(...), the one shared copy of the formula for tests. AuditorCommitReveal moves its helper and one inline hash onto it and adds three tests:• test_copyAttack_replayingPeerCommitAndReveal_reverts• test_commitHashBoundToSlot_otherModelBatchOrGI_reverts• test_honestPath_boundHash_allRevealAndClose |
| Functionality — how & why | How: • Copy attack: B commits A's stored hash and reveals A's exact values. TA_RevealHashMismatch, and B gets no hasAuditedLM.• Slot test: three auditors commit on model 0 hashes built for another model, batch or GI, and every reveal reverts. • Honest path: all commit and reveal their own hashes, and the median closes at 70. Why: these pin the No. 192 fix and the slot binding, and show the honest flow still finalizes. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merge as-is. Break-then-fix: dropping the msg.sender + modelIndex binding (contract and helper) fails exactly the copy-attack and slot tests; the honest-path test still passes. |
| Actual merge proposal | Both applied as-is (helper copied), byte-identical to PR head; the three new tests pass in the full run. Evidence on the merged tree (develop @ 420d48d + this PR): forge clean && forge build (via_ir) clean; size gate exit 0; forge test 484 passed / 0 failed / 0 skipped; pytest -m "not integration" 389 passed / 0 failed. Uncommitted in the develop cwd, not committed or pushed. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
foundry/test/{AggregatorCommitReveal,DisputeResolution,GasSimulation,LifecycleEvents,PR146SlashingRegression,RewardEngine,ScoringValidation,SecurityFindings,StakingEnforcement,TreasuryForwarding}.t.sol
| Field | Value |
|---|---|
| Change | Modified (10 files) |
| Lines | +2/-2, +2/-4, +8/-14, +3/-4, +2/-2, +3/-8, +2/-4, +7/-12, +2/-2, +2/-2 |
| Diff (what exactly is in this PR) | Imports auditCommitHash, and every commitAuditScore fixture call now hashes with the pranked auditor and its own (gi, batchId, modelIndex). Most of these used to build one hash before the auditor loop and reuse it. |
| Functionality — how & why | How: fixture-only; no assertions change. Why: a shared hash no longer reveals for anyone but the auditor it was built for, so every fixture that drives a GI through evaluation would otherwise revert at reveal. |
Diff vs current develop HEAD |
None. AggregatorCommitReveal.t.sol and StakingEnforcement.t.sol are also touched by PR No. 211, in different hunks. The combined merge is clean and its full suite is green. |
| Recommended merge proposal | Merge as-is. All 25 rewritten sites checked by hand: each hashes the pranked auditor and the committed slot. 479/479 on this branch. |
| Actual merge proposal | All 10 applied cleanly. 8 are byte-identical to PR head. AggregatorCommitReveal.t.sol and StakingEnforcement.t.sol also carry PR No. 211's hunks, which landed first; both match git's own 3-way merge (git merge-tree --write-tree) byte-for-byte. Evidence on the merged tree (develop @ 420d48d + this PR): forge clean && forge build (via_ir) clean; size gate exit 0; forge test 484 passed / 0 failed / 0 skipped; pytest -m "not integration" 389 passed / 0 failed. Uncommitted in the develop cwd, not committed or pushed. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
Documentation/technical/contracts/DINTaskAuditor.md, Documentation/technical/contracts/DINShared.md, Documentation/technical/audits/foundry-src-security-review.md
| Field | Value |
|---|---|
| Change | Modified (all three) |
| Lines | +3/-2, +2/-2, +1/-1 |
| Diff (what exactly is in this PR) | • DINTaskAuditor.md: the §7 formula, §13 No. 2 marked fixed, and a Change Log entry.• DINShared.md: the TA_RevealHashMismatch row and the commit-reveal section.• Security review: the M-1 note now records the auditor side as fixed for No. 192. |
| Functionality — how & why | How / why: keeps the code-reader docs in line with develop. The repo-wide grep finds no Documentation/ page still giving the old formula. |
Diff vs current develop HEAD |
None |
| Recommended merge proposal | Merge as-is. Docs CI job green. Optional nit: DINTaskAuditor.md §7 and §13 No. 2 say issue #192, while its Change Log and the rest of the file use issue No. N. |
| Actual merge proposal | All three applied. DINShared.md and the security review are byte-identical to PR head. DINTaskAuditor.md also takes the optional review nit: (issue #192) becomes (issue No. 192) in §7 and §13 No. 2, matching its Change Log and other issue refs. Only those two lines differ from PR head. check_doc_links.py on Documentation (258) and Developer (242): all resolve. Uncommitted in the develop cwd, not committed or pushed. |
| Pending proposal | None |
| Local merge conflict | No |
| GitHub merge conflict | No |
Verification
All on the real via_ir profile:
forge test: 479 passed / 0 failed (39 suites).pytest -m "not integration"with torch: 389 passed.DINTaskAuditor: 22,589 B (1,987 margin).- Golden vectors re-derived with
cast. - Break-then-fix on both the Python hash and the contract bindings.
- Combined with PR No. 211: 484 passed / 0 failed, size gate green (two warnings).
Full detail is in the verification comment above.
Local vs. GitHub agree: both report a clean merge with no conflicts.
Actual outcome — PR No. 212 merged + review nit applied (pushed)This supersedes the pre-merge review comment above with what actually happened when landing this PR on Two commits, both on
Files unchanged from the PR (17 of 18)
Files changed by
|
| File | What changed vs. this PR's merged version |
|---|---|
Documentation/technical/contracts/DINTaskAuditor.md |
§7 and §13 No. 2 now say issue No. 192 instead of issue #192, matching the file's Change Log entry and its other issue references. Only those two lines changed. |
Why the change
It's the optional wording nit from the review, not develop drift. Nothing in this PR needed a functional fix.
Verification
On the pushed tree, 7785acf (PR No. 211 + this PR):
forge clean && forge build(via_ir) is clean.contract_size_gate.pyexits 0.DINTaskAuditoris 22,589 B (1,987 B margin) andDINTaskCoordinator23,497 B (1,079 B), both in the warning band.forge test: 484 passed, 0 failed.pytest -m "not integration"with an empty HOME: 389 passed, 0 failed.dump-abi --officialleaves both task ABIs unchanged.- Doc links in
Documentation(258) andDeveloper(242) all resolve. - The local CI mirror on
7785acfpassed: docs, pytest, via_ir build, forge test and hardhat. - GitHub
pushrun ondevelop: green, including thecontract size gatestep.
Local vs. GitHub agree: clean merge with no conflicts, as both predicted. Issue No. 192 is fixed by this PR.
Summary
PR 2 of task_021026_19, Part C. It implements amendments 1–6 of the #192 approval review. Closes #192.
The attack:
revealAuditScorecheckedkeccak256(abi.encodePacked(score, vote, salt)), which carries no address, GI, batch or model. An assigned auditor could copy a peer's commit hash, which is public, wait for the peer's reveal, and replay it. That earned a quorum vote and reward weight with no evaluation.The fix: the hash is now
keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex)), matching the aggregation side (#156 M-1). Function signatures don't change, and the bundleddincli/abis/DINTaskAuditor.jsonis unchanged: I regenerated it and the diff was empty.Changes by amendment
DINTaskAuditor.sol): the new formula inrevealAuditScore. The NatSpec that describes the formula is updated at the state comment, at thecommitAuditScore@dev, and at its@param._audit_commit_hash(score, vote, salt, sender, gi, batch_id, model_index)indincli/cli/auditor.py. It useseth_abi.encode+Web3.keccak, modelled on_agg_commit_hash, andevaluate --submitnow calls it.reveal_lmssends the stored values and is unchanged.foundry/test/utils/AuditCommitHash.sol, asauditCommitHash(...).commitAuditScorecall sites in 11 files now hash per pranked auditor. Before, most built one hash and reused it across the auditor loop.AuditorCommitReveal.t.sol:test_copyAttack_replayingPeerCommitAndReveal_reverts: B commits A's hash and reveals A's(score, vote, salt). It reverts withTA_RevealHashMismatch, and B gets no vote.test_commitHashBoundToSlot_otherModelBatchOrGI_reverts: a hash built for another model, batch or GI doesn't reveal.test_honestPath_boundHash_allRevealAndClose: every auditor reveals its own hash, and evaluation closes with the expected score.tests/test_auditor_commit_hash.pypins two golden vectors computed independently withcast abi-encodeandcast keccak, and checks that the hash varies with the sender and with the slot.DINTaskAuditor.md: §7, §13 No. 2 marked fixed, and a change-log entry.DINShared.md: theTA_RevealHashMismatchrow and the commit-reveal section.DINTaskAuditorruntime goes from 22,567 B to 22,589 B (+22 B), a 1,987 B margin. That clears Contract size headroom for all contracts (DINTaskCoordinator −9 B over EIP-170 with PR No. 197) + slash reason for committed-but-unrevealed across commit-reveal flows #201's 1,024 B budget but sits in the 2,048 B warning band.hardhat/contracts/DINTaskAuditor.sol(the stale mirror) is left alone, as the approval review allows.Relation to #211
develop, independent of fix(task-coordinator): back under EIP-170 with a CI size gate (#201 Part A) + onlyCurrentGI on registerDINaggregator (#206) #211, andgit merge-treeagainst fix(task-coordinator): back under EIP-170 with a CI size gate (#201 Part A) + onlyCurrentGI on registerDINaggregator (#206) #211's head is clean.DINTaskCoordinatorhere is stilldevelop's 24,585 B; fix(task-coordinator): back under EIP-170 with a CI size gate (#201 Part A) + onlyCurrentGI on registerDINaggregator (#206) #211 fixes that. This PR doesn't touch the coordinator.DINTaskAuditorat 1,987 B.Migration note
Any commit made under the old formula can't be revealed after this lands. That isn't a live concern, because
DINTaskAuditorfromdevelopisn't deployed on any persistent network.Verification
forge clean && forge build && forge test: 479 passed, 0 failed (39 suites).pytest -m "not integration": 313 passed, including the 4 new tests. The torch-dependent files and the 2test_integration_markerchecks that collect them can't run in my local venv (no torch), the same as ondevelop. CI installs the full deps.forge lint src/DINTaskAuditor.sol: 1 warning, the same pre-existingdivide-before-multiplyas ondevelop.check_doc_links.py Documentation: all links resolve.🤖 Generated with Claude Code