diff --git a/Documentation/technical/audits/foundry-src-security-review.md b/Documentation/technical/audits/foundry-src-security-review.md index 07c9719..14dc09d 100644 --- a/Documentation/technical/audits/foundry-src-security-review.md +++ b/Documentation/technical/audits/foundry-src-security-review.md @@ -151,7 +151,7 @@ The caller of these functions (the model owner, since both are gated `onlyOwner` ### M-1. No commit-reveal on aggregation/scoring submissions — "copy the leader" free-riding -**Fixed — auditor scoring: PR #63 (task_210726_6 §2a, predates this report's follow-up numbering). Aggregation-side (`submitT1Aggregation`/`submitT2Aggregation`, described below): [PR #197](https://github.com/InfiniteZeroFoundation/DevNet/pull/197) (issue #156, task_240926_18 Part C).** `commitT1Aggregation`/`revealT1Aggregation` and `commitT2Aggregation`/`revealT2Aggregation` replace the single-shot submit functions; only revealed CIDs count toward finalization, closing the "read every prior submission, then copy the leader" path this finding describes. The commit hash binds `msg.sender` (`keccak256(abi.encode(cid, salt, msg.sender, GI, tierKind, batchId))`), deliberately hardening past this finding's own `keccak256(cid, salt)` recommendation — without the sender binding, a lazy aggregator could copy a peer's *commit hash* itself and reveal the peer's `(cid, salt)` under their own name once the peer reveals, reproducing the same free-riding this fix is meant to close. PR #63's auditor-side commit hash (`keccak256(abi.encodePacked(score, vote, salt))`) has this same unbound-sender weakness and was **not** fixed as part of this PR — see the new issue opened for it, linked from the PR. +**Fixed — auditor scoring: PR #63 (task_210726_6 §2a, predates this report's follow-up numbering). Aggregation-side (`submitT1Aggregation`/`submitT2Aggregation`, described below): [PR #197](https://github.com/InfiniteZeroFoundation/DevNet/pull/197) (issue #156, task_240926_18 Part C).** `commitT1Aggregation`/`revealT1Aggregation` and `commitT2Aggregation`/`revealT2Aggregation` replace the single-shot submit functions; only revealed CIDs count toward finalization, closing the "read every prior submission, then copy the leader" path this finding describes. The commit hash binds `msg.sender` (`keccak256(abi.encode(cid, salt, msg.sender, GI, tierKind, batchId))`), deliberately hardening past this finding's own `keccak256(cid, salt)` recommendation — without the sender binding, a lazy aggregator could copy a peer's *commit hash* itself and reveal the peer's `(cid, salt)` under their own name once the peer reveals, reproducing the same free-riding this fix is meant to close. PR #63's auditor-side commit hash (`keccak256(abi.encodePacked(score, vote, salt))`) had this same unbound-sender weakness and was not fixed in PR #197. It was fixed separately for issue #192: the auditor hash is now `keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex))`. **Contracts / functions:** `DINTaskCoordinator.submitT1Aggregation()` / `submitT2Aggregation()` (L520-543, L600-622); `DINTaskAuditor.setAuditScorenEligibility()` (L515-544). diff --git a/Documentation/technical/contracts/DINShared.md b/Documentation/technical/contracts/DINShared.md index 4720a69..9102870 100644 --- a/Documentation/technical/contracts/DINShared.md +++ b/Documentation/technical/contracts/DINShared.md @@ -213,7 +213,7 @@ Used by: `DINTaskCoordinator` | `TA_EmptyCommitHash` | `commitHash` argument is `bytes32(0)` | | `TA_RevealPhaseNotOpen` | `revealAuditScore` called while `GIstate != LMSevaluationRevealStarted` | | `TA_NoCommitFound` | No prior `commitAuditScore` recorded for this auditor/model — reveal without a commit | -| `TA_RevealHashMismatch` | `keccak256(abi.encodePacked(score, vote, salt))` does not match the stored commit hash | +| `TA_RevealHashMismatch` | `keccak256(abi.encode(score, vote, salt, auditor, gi, batchId, modelIndex))`, with `auditor` = `msg.sender`, does not match the stored commit hash | | `TC_RevealCannotBeStarted` | `startLMsubmissionsEvaluationReveal` called while `GIstate != LMSevaluationStarted` | | `TA_EncryptedKeyCountMismatch` | `assignAuditTestDataset`'s `encryptedKeys` array length does not match the batch's auditor count | @@ -398,4 +398,4 @@ The `TA_` and `TC_` prefixes make it immediately clear in stack traces and event ### Commit-Then-Reveal Auditor Scoring -`LMSevaluationStarted` and `LMSevaluationRevealStarted` split what was previously a single evaluation phase into two: auditors first commit `keccak256(score, vote, salt)` (hiding their vote from other auditors until everyone has committed), then, once the model owner closes the commit window via `DINTaskCoordinator.startLMsubmissionsEvaluationReveal`, reveal the underlying `(score, vote, salt)` for it to be counted. An auditor who commits but never reveals is simply excluded from quorum/median counting, and remains slashable via the existing "missed vote" check in `slashAuditors` — no separate non-reveal handling needed. +`LMSevaluationStarted` and `LMSevaluationRevealStarted` split what was previously a single evaluation phase into two: auditors first commit `keccak256(abi.encode(score, vote, salt, auditor, gi, batchId, modelIndex))` (hiding their vote from other auditors until everyone has committed), then, once the model owner closes the commit window via `DINTaskCoordinator.startLMsubmissionsEvaluationReveal`, reveal the underlying `(score, vote, salt)` for it to be counted. An auditor who commits but never reveals is simply excluded from quorum/median counting, and remains slashable via the existing "missed vote" check in `slashAuditors` — no separate non-reveal handling needed. diff --git a/Documentation/technical/contracts/DINTaskAuditor.md b/Documentation/technical/contracts/DINTaskAuditor.md index cca40ee..197e269 100644 --- a/Documentation/technical/contracts/DINTaskAuditor.md +++ b/Documentation/technical/contracts/DINTaskAuditor.md @@ -120,7 +120,7 @@ The reward split and the S1 fraction are explicitly provisional (MECHANISM_DESIG ## 7. Commit-then-Reveal Scoring -1. **Commit** (`LMSevaluationStarted`): `commitAuditScore(gi, batchId, modelIndex, commitHash)` with `commitHash = keccak256(abi.encodePacked(score, vote, salt))`. The caller must be an assigned, active auditor; one commit each; a zero hash is rejected. +1. **Commit** (`LMSevaluationStarted`): `commitAuditScore(gi, batchId, modelIndex, commitHash)` with `commitHash = keccak256(abi.encode(score, vote, salt, auditor, gi, batchId, modelIndex))`, where `auditor` is the committing address. Binding the auditor and the slot means a peer can't copy the hash and replay the reveal, and one commit can't be reused for another model, batch or GI (issue #192). The caller must be an assigned, active auditor; one commit each; a zero hash is rejected. 2. **Reveal** (`LMSevaluationRevealStarted`): `revealAuditScore(gi, batchId, modelIndex, score, vote, salt)`. Checks the auditor is active, `score ≤ 100`, a commit exists, no prior reveal, and the hash matches. Records the score and vote, sets `hasAuditedLM`, increments `auditorGIWeight` / `giTotalAuditWeight` (the auditor reward basis), and tries to finalize eligibility. 3. **Eligibility** (`_tryFinalizeEligibility`): once revealed votes ≥ `minEligibilityQuorum`, `eligible = (yesVotes ≥ minEligibilityQuorum)`. With the defaults that means 2 "yes" votes out of 3. 4. **`finalizeEvaluation(gi)`** (from the coordinator's `closeLMsubmissionsEvaluation`, still in the reveal state): for each batch model with ≥ `minScoreQuorum` revealed scores, `finalMedianScore = median`, `evaluated = true`, `approved = eligible && median ≥ passScore`. The first time a model is approved, its median is added to `giTotalApprovedScore` (the client reward basis). Emits `AuditorScoreDeviation` for every revealing auditor (S3 shadow data). Returns `true` if at least one model reached quorum. @@ -196,7 +196,7 @@ Registration & data: `DINAuditorRegistered`, `LocalModelSubmitted`, `AuditorsBat Read alongside the [foundry/src security review](../audits/foundry-src-security-review.md). - **No. 1 — Test-data disputes can be won by the challenger alone.** `resolveTestDataDispute` is callable by anyone, and any commitment *mismatch* upholds the dispute. A challenger can call it with an arbitrary `K` and win: bond back, the model owner's GI pool cut by `disputePenaltyBps`, and the batch blocked until reassignment. Only the model owner revealing the real `K` should be able to reach the "match" branch, and a mismatch from a non-owner caller should not count as evidence. `disputeBondAmount` defaults to 0, so this costs the challenger nothing and can be repeated after every reassignment. Tracked in issue No. 205. -- **No. 2 — Commit hashes are not bound to the auditor.** `keccak256(score, vote, salt)` carries no address, GI, batch or model. An auditor in the same batch can copy another's commit hash, wait for their reveal, and replay it. Tracked in issue No. 192. (The aggregation commits on the coordinator do bind `msg.sender`.) +- **No. 2 — Fixed: commit hashes are now bound to the auditor.** The old hash, `keccak256(abi.encodePacked(score, vote, salt))`, carried no address, GI, batch or model. An assigned auditor could copy a peer's commit hash, wait for the peer's reveal, and replay it for a free vote. The hash now binds `msg.sender`, `gi`, `batchId` and `modelIndex` (§7), as the aggregation commits on the coordinator do (issue #192). - **No. 3 — Committed-but-unrevealed is slashed as a liveness miss.** An auditor who commits and then withholds the reveal pays the S1 fraction (`AUD_NO_VOTE`, 30% of `minStake` by default). That is less than the full-`minStake` S3 slash a revealed outlier would pay once `s3SlashingEnabled` is on, so an auditor who sees they will be in the minority can choose not to reveal. Whether this case gets its own reason code and fraction is open in issue No. 201 (Part B). - **No. 4 — Unclaimable remainders.** If no model is approved (`giTotalApprovedScore == 0`), or nobody reveals, or no aggregator weight exists, that role's pool share stays in the contract with no reclaim path. - **No. 5 — `setTestDataAssignedFlag` gates nothing:** scoring can open before any test data is assigned. @@ -218,3 +218,4 @@ Read alongside the [foundry/src security review](../audits/foundry-src-security- - Registration caps and floors, the active-registration counter, and the `modelId` constructor argument. - Treasury shares and forfeitures forwarded to the platform treasury (`slashTreasury()`), replacing the per-contract treasury address (issue No. 152). - `createAuditorsBatches` takes the coordinator's locked audit seed (issue No. 156 H-2, PR No. 191). +- The audit commit hash binds the auditor and the slot: `keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex))` replaces `keccak256(abi.encodePacked(score, vote, salt))` (issue No. 192). Function signatures and the ABI are unchanged; in-flight commits made under the old formula can't be revealed after the switch. diff --git a/dincli/cli/auditor.py b/dincli/cli/auditor.py index c3a6008..7e30391 100644 --- a/dincli/cli/auditor.py +++ b/dincli/cli/auditor.py @@ -7,6 +7,7 @@ import typer from rich.table import Table from web3 import Web3 +from eth_abi import encode as abi_encode from nacl.public import Box, PrivateKey, PublicKey import nacl.encoding from cryptography.hazmat.primitives.ciphers.aead import AESGCM @@ -69,6 +70,18 @@ def _load_commit(model_base_dir: Path, gi: int, batch_id: int, model_index: int) data["salt"] = bytes.fromhex(data["salt"]) return data + +def _audit_commit_hash(score: int, vote: bool, salt: bytes, sender: str, gi: int, batch_id: int, model_index: int) -> bytes: + """keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex)) -- + must match DINTaskAuditor.revealAuditScore exactly, including plain + (non-packed) ABI encoding. Binding the auditor and the (gi, batch, model) + slot stops a peer replaying this auditor's commit and reveal (issue #192).""" + encoded = abi_encode( + ["uint256", "bool", "bytes32", "address", "uint256", "uint256", "uint256"], + [score, vote, salt, Web3.to_checksum_address(sender), gi, batch_id, model_index], + ) + return Web3.keccak(encoded) + app = typer.Typer(help="Commands for Auditors in DIN.") dintoken_app = typer.Typer(help="Commands for DIN Token in DIN.") @@ -531,8 +544,8 @@ def evaluate_lms( score_int = int(score) vote_bool = bool(eligible) salt = secrets.token_bytes(32) - commit_hash = Web3.solidity_keccak( - ["uint256", "bool", "bytes32"], [score_int, vote_bool, salt] + commit_hash = _audit_commit_hash( + score_int, vote_bool, salt, account.address, curr_GI, batch_id, model_index ) try: diff --git a/foundry/src/DINTaskAuditor.sol b/foundry/src/DINTaskAuditor.sol index fd5df32..b3f6455 100644 --- a/foundry/src/DINTaskAuditor.sol +++ b/foundry/src/DINTaskAuditor.sol @@ -241,7 +241,10 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { mapping(uint256 => mapping(uint => mapping(address => mapping(uint => bool)))) // GI // batchId // auditor // modelIndex // has voted public hasAuditedLM; - // Commit-then-reveal (task_210726_6 §2a). commitHash = keccak256(abi.encodePacked(score, vote, salt)). + // Commit-then-reveal (task_210726_6 §2a). commitHash = + // keccak256(abi.encode(score, vote, salt, auditor, gi, batchId, modelIndex)): + // binding the auditor and (gi, batchId, modelIndex) stops a peer copying + // another auditor's commit hash and reveal (issue #192). // hasCommittedLM is distinct from hasAuditedLM: hasAuditedLM is set only // on a successful reveal and remains the single source of truth for // quorum/median counting, exactly as before -- an auditor who commits @@ -1077,8 +1080,12 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { /// @notice Phase 1 of commit-then-reveal auditor scoring: lock in a /// hidden (score, vote) pair. /// @dev Caller must be the assigned auditor for this batch and model - /// index. `commitHash` must equal `keccak256(abi.encodePacked(score, - /// vote, salt))` for the values the auditor intends to reveal later + /// index. `commitHash` must equal `keccak256(abi.encode(score, vote, + /// salt, msg.sender, gi, batchId, modelIndex))` for the values the + /// auditor intends to reveal later. Binding the auditor's own address + /// and the (gi, batchId, modelIndex) slot means a peer can't copy this + /// hash and later replay this auditor's reveal (issue #192), and an + /// auditor can't reuse one commit for another model, batch or GI /// -- the contract cannot and does not validate this at commit time /// (that's the point; nothing about score/vote is visible yet). /// Open only while GIstate == LMSevaluationStarted (the commit @@ -1088,7 +1095,7 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { /// @param gi Current GI index. /// @param batchId Batch index containing this model. /// @param modelIndex Index into lmSubmissions[gi] for the model being scored. - /// @param commitHash keccak256(abi.encodePacked(score, vote, salt)). + /// @param commitHash keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex)). function commitAuditScore( uint256 gi, uint batchId, @@ -1157,7 +1164,9 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { if (hasAuditedLM[gi][batchId][msg.sender][modelIndex]) revert TA_AlreadyVoted(); - bytes32 expectedHash = keccak256(abi.encodePacked(score, vote, salt)); + bytes32 expectedHash = keccak256( + abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex) + ); if ( expectedHash != auditScoreCommits[gi][batchId][msg.sender][modelIndex] diff --git a/foundry/test/AggregatorCommitReveal.t.sol b/foundry/test/AggregatorCommitReveal.t.sol index 52ad081..5d49658 100644 --- a/foundry/test/AggregatorCommitReveal.t.sol +++ b/foundry/test/AggregatorCommitReveal.t.sol @@ -37,6 +37,7 @@ import { TC_T2RevealHashMismatch, TC_T2RevealPhaseNotOpen } from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract AggregatorCommitRevealTest is Test { DinToken tokenImpl; @@ -219,11 +220,10 @@ contract AggregatorCommitRevealTest is Test { vm.stopPrank(); (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); - bytes32 scoreCommit = keccak256(abi.encodePacked(uint256(100), true, TEST_SALT)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], scoreCommit); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(100), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } diff --git a/foundry/test/AuditorCommitReveal.t.sol b/foundry/test/AuditorCommitReveal.t.sol index 5c44eb1..d1963ce 100644 --- a/foundry/test/AuditorCommitReveal.t.sol +++ b/foundry/test/AuditorCommitReveal.t.sol @@ -17,7 +17,8 @@ import {DinValidatorStake} from "../src/DinValidatorStake.sol"; import {DINModelRegistry} from "../src/DINModelRegistry.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; -import {GIstates} from "../src/DINShared.sol"; +import {GIstates, TA_RevealHashMismatch} from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract AuditorCommitRevealTest is Test { DinToken tokenImpl; @@ -199,9 +200,8 @@ contract AuditorCommitRevealTest is Test { // ───────────────────────────────────────────────────────────────────── function _commitScore(address who, uint gi, uint batchId, uint modelIdx, uint256 score, bool vote) internal { - bytes32 hash = keccak256(abi.encodePacked(score, vote, TEST_SALT)); vm.prank(who); - ta.commitAuditScore(gi, batchId, modelIdx, hash); + ta.commitAuditScore(gi, batchId, modelIdx, auditCommitHash(score, vote, TEST_SALT, who, gi, batchId, modelIdx)); } function _revealScore(address who, uint gi, uint batchId, uint modelIdx, uint256 score, bool vote) internal { @@ -291,7 +291,7 @@ contract AuditorCommitRevealTest is Test { vm.prank(batchAuditors[0]); vm.expectRevert(); // TA_AlreadyCommitted - ta.commitAuditScore(1, 0, modelIdxs[0], keccak256(abi.encodePacked(uint256(90), true, TEST_SALT))); + ta.commitAuditScore(1, 0, modelIdxs[0], auditCommitHash(uint256(90), true, TEST_SALT, batchAuditors[0], 1, 0, modelIdxs[0])); } function test_reveal_twiceReverts() public { @@ -338,6 +338,87 @@ contract AuditorCommitRevealTest is Test { assertFalse(ta.hasAuditedLM(1, 0, batchAuditors[2], modelIdxs[0]), "but never revealed/counted"); } + // ───────────────────────────────────────────────────────────────────── + // Issue #192: the commit hash binds the auditor and (gi, batchId, + // modelIndex), so a copied commit can't be revealed by the copier and a + // commit can't be reused for another slot. + // ───────────────────────────────────────────────────────────────────── + + /// @dev The #192 attack: B copies A's public commit hash, waits for A's + /// reveal, then reveals A's exact (score, vote, salt). With the old + /// sender-independent hash this succeeded and B earned a free vote. + function test_copyAttack_replayingPeerCommitAndReveal_reverts() public { + _runToLMSevaluationStarted(); + (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); + address honest = batchAuditors[0]; + address copier = batchAuditors[1]; + + _commitScore(honest, 1, 0, modelIdxs[0], 80, true); + bytes32 honestHash = ta.auditScoreCommits(1, 0, honest, modelIdxs[0]); + vm.prank(copier); + ta.commitAuditScore(1, 0, modelIdxs[0], honestHash); + + _openRevealPhase(1); + _revealScore(honest, 1, 0, modelIdxs[0], 80, true); + + vm.prank(copier); + vm.expectRevert(TA_RevealHashMismatch.selector); + ta.revealAuditScore(1, 0, modelIdxs[0], 80, true, TEST_SALT); + assertFalse(ta.hasAuditedLM(1, 0, copier, modelIdxs[0]), "copier gets no vote"); + } + + /// @dev A hash built for one (gi, batchId, modelIndex) doesn't reveal for + /// any other: another model, another batch, or another GI. + function test_commitHashBoundToSlot_otherModelBatchOrGI_reverts() public { + _runToLMSevaluationStarted(); + (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); + require(modelIdxs.length >= 2, "fixture: need 2 models in batch 0"); + address a0 = batchAuditors[0]; + address a1 = batchAuditors[1]; + address a2 = batchAuditors[2]; + + // Each commits on modelIdxs[0] a hash built for a different slot. + vm.prank(a0); + ta.commitAuditScore(1, 0, modelIdxs[0], auditCommitHash(80, true, TEST_SALT, a0, 1, 0, modelIdxs[1])); + vm.prank(a1); + ta.commitAuditScore(1, 0, modelIdxs[0], auditCommitHash(80, true, TEST_SALT, a1, 1, 1, modelIdxs[0])); + vm.prank(a2); + ta.commitAuditScore(1, 0, modelIdxs[0], auditCommitHash(80, true, TEST_SALT, a2, 2, 0, modelIdxs[0])); + + _openRevealPhase(1); + for (uint i = 0; i < 3; i++) { + vm.prank(batchAuditors[i]); + vm.expectRevert(TA_RevealHashMismatch.selector); + ta.revealAuditScore(1, 0, modelIdxs[0], 80, true, TEST_SALT); + } + } + + /// @dev The honest path still works end to end with the bound hash: every + /// auditor commits and reveals its own hash, and evaluation closes + /// with the expected aggregate score. + function test_honestPath_boundHash_allRevealAndClose() public { + _runToLMSevaluationStarted(); + (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); + + for (uint i = 0; i < batchAuditors.length; i++) { + for (uint m = 0; m < modelIdxs.length; m++) { + _commitScore(batchAuditors[i], 1, 0, modelIdxs[m], 70, true); + } + } + _openRevealPhase(1); + for (uint i = 0; i < batchAuditors.length; i++) { + for (uint m = 0; m < modelIdxs.length; m++) { + _revealScore(batchAuditors[i], 1, 0, modelIdxs[m], 70, true); + assertTrue(ta.hasAuditedLM(1, 0, batchAuditors[i], modelIdxs[m])); + } + } + + vm.prank(modelOwner); + tc.closeLMsubmissionsEvaluation(1); + (, , , , , , uint256 finalScore) = ta.lmSubmissions(1, modelIdxs[0]); + assertEq(finalScore, 70); + } + // ───────────────────────────────────────────────────────────────────── // §2b: per-validator encrypted test-data key mapping. // ───────────────────────────────────────────────────────────────────── diff --git a/foundry/test/DisputeResolution.t.sol b/foundry/test/DisputeResolution.t.sol index 564caf9..7af9d89 100644 --- a/foundry/test/DisputeResolution.t.sol +++ b/foundry/test/DisputeResolution.t.sol @@ -20,6 +20,7 @@ import {DINModelRegistry} from "../src/DINModelRegistry.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import "../src/DINShared.sol" as Shared; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; // Pull the custom errors into scope so vm.expectRevert(Error.selector) compiles. error TC_DisputeNotAwaitingRecomputation(); @@ -245,11 +246,8 @@ contract DisputeResolutionTest is Test { .getAuditorsBatch(1, 0); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { - bytes32 commitHash = keccak256( - abi.encodePacked(uint256(100), true, TEST_SALT) - ); vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(100), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } diff --git a/foundry/test/GasSimulation.t.sol b/foundry/test/GasSimulation.t.sol index 01dc889..32b1c8c 100644 --- a/foundry/test/GasSimulation.t.sol +++ b/foundry/test/GasSimulation.t.sol @@ -11,6 +11,7 @@ import {DinValidatorStake} from "../src/DinValidatorStake.sol"; import {DinTreasury} from "../src/DinTreasury.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; /// @notice Gas simulation for issue #78 — validator network-fee sizing. /// @@ -162,13 +163,12 @@ contract GasSimulationTest is Test { /// T1AggregationStarted. function _completeEvalAndOpenT1(uint gi) internal { uint bCnt = ta.AuditorsBatchCount(gi); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(75), true, TEST_SALT)); for (uint b = 0; b < bCnt; b++) { (, address[] memory bAuds, uint[] memory bMods,) = ta.getAuditorsBatch(gi, b); for (uint a = 0; a < bAuds.length; a++) { for (uint m = 0; m < bMods.length; m++) { vm.prank(bAuds[a]); - ta.commitAuditScore(gi, b, bMods[m], commitHash); + ta.commitAuditScore(gi, b, bMods[m], auditCommitHash(uint256(75), true, TEST_SALT, bAuds[a], gi, b, bMods[m])); } } } @@ -367,8 +367,7 @@ contract GasSimulationTest is Test { _setupToEvalStart(3); (, address[] memory bAuds, uint[] memory bMods,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(75), true, TEST_SALT)); - vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], commitHash); + vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], auditCommitHash(uint256(75), true, TEST_SALT, bAuds[0], 1, 0, bMods[0])); tc.startLMsubmissionsEvaluationReveal(1); uint before = gasleft(); @@ -381,10 +380,8 @@ contract GasSimulationTest is Test { _setupToEvalStart(3); (, address[] memory bAuds, uint[] memory bMods,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash0 = keccak256(abi.encodePacked(uint256(70), true, TEST_SALT)); - bytes32 commitHash1 = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); - vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], commitHash0); - vm.prank(bAuds[1]); ta.commitAuditScore(1, 0, bMods[0], commitHash1); + vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], auditCommitHash(uint256(70), true, TEST_SALT, bAuds[0], 1, 0, bMods[0])); + vm.prank(bAuds[1]); ta.commitAuditScore(1, 0, bMods[0], auditCommitHash(uint256(80), true, TEST_SALT, bAuds[1], 1, 0, bMods[0])); tc.startLMsubmissionsEvaluationReveal(1); // First reveal (cold, below quorum) @@ -402,10 +399,9 @@ contract GasSimulationTest is Test { function test_gas_s2_commitAuditScore_cold_3batches() public { _setupToEvalStart(3); (, address[] memory bAuds, uint[] memory bMods,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(75), true, TEST_SALT)); uint before = gasleft(); - vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], commitHash); + vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], auditCommitHash(uint256(75), true, TEST_SALT, bAuds[0], 1, 0, bMods[0])); console.log("[GAS][S2] commitAuditScore (cold, LOW 3 batches):", before - gasleft()); } @@ -413,10 +409,9 @@ contract GasSimulationTest is Test { function test_gas_s2_commitAuditScore_cold_5batches() public { _setupToEvalStart(5); (, address[] memory bAuds, uint[] memory bMods,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(75), true, TEST_SALT)); uint before = gasleft(); - vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], commitHash); + vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], auditCommitHash(uint256(75), true, TEST_SALT, bAuds[0], 1, 0, bMods[0])); console.log("[GAS][S2] commitAuditScore (cold, MID 5 batches):", before - gasleft()); } @@ -424,10 +419,9 @@ contract GasSimulationTest is Test { function test_gas_s2_commitAuditScore_cold_10batches() public { _setupToEvalStart(10); (, address[] memory bAuds, uint[] memory bMods,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(75), true, TEST_SALT)); uint before = gasleft(); - vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], commitHash); + vm.prank(bAuds[0]); ta.commitAuditScore(1, 0, bMods[0], auditCommitHash(uint256(75), true, TEST_SALT, bAuds[0], 1, 0, bMods[0])); console.log("[GAS][S2] commitAuditScore (cold, HIGH 10 batches):", before - gasleft()); } diff --git a/foundry/test/LifecycleEvents.t.sol b/foundry/test/LifecycleEvents.t.sol index ad1aecf..75f443f 100644 --- a/foundry/test/LifecycleEvents.t.sol +++ b/foundry/test/LifecycleEvents.t.sol @@ -23,6 +23,7 @@ import {DinValidatorStake} from "../src/DinValidatorStake.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract LifecycleEventsTest is Test { DinToken token; @@ -229,9 +230,8 @@ contract LifecycleEventsTest is Test { (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(giIndex, ab); for (uint ai = 0; ai < batchAuditors.length; ai++) { for (uint mi = 0; mi < modelIdxs.length; mi++) { - bytes32 ch = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); vm.prank(batchAuditors[ai]); - ta.commitAuditScore(giIndex, ab, modelIdxs[mi], ch); + ta.commitAuditScore(giIndex, ab, modelIdxs[mi], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[ai], giIndex, ab, modelIdxs[mi])); } } } @@ -580,9 +580,8 @@ contract LifecycleEventsTest is Test { (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); for (uint ai = 0; ai < batchAuditors.length; ai++) { for (uint mi = 0; mi < modelIdxs.length; mi++) { - bytes32 ch = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); vm.prank(batchAuditors[ai]); - ta.commitAuditScore(1, 0, modelIdxs[mi], ch); + ta.commitAuditScore(1, 0, modelIdxs[mi], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[ai], 1, 0, modelIdxs[mi])); } } diff --git a/foundry/test/PR146SlashingRegression.t.sol b/foundry/test/PR146SlashingRegression.t.sol index 7178391..dfb2c58 100644 --- a/foundry/test/PR146SlashingRegression.t.sol +++ b/foundry/test/PR146SlashingRegression.t.sol @@ -21,6 +21,7 @@ import {DinTreasury} from "../src/DinTreasury.sol"; import {DinFeeRouter} from "../src/DinFeeRouter.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract PR146SlashingRegressionTest is Test { // ───────────────────────────────────────────────────────────────────── @@ -194,11 +195,10 @@ contract PR146SlashingRegressionTest is Test { // Only batch 0 votes -- batch 1's auditors miss their vote entirely. (, address[] memory batch0Auditors, uint[] memory batch0Models, ) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(100), true, TEST_SALT)); for (uint i = 0; i < batch0Auditors.length; i++) { for (uint m = 0; m < batch0Models.length; m++) { vm.prank(batch0Auditors[i]); - ta.commitAuditScore(1, 0, batch0Models[m], commitHash); + ta.commitAuditScore(1, 0, batch0Models[m], auditCommitHash(uint256(100), true, TEST_SALT, batch0Auditors[i], 1, 0, batch0Models[m])); } } diff --git a/foundry/test/RewardEngine.t.sol b/foundry/test/RewardEngine.t.sol index 08a86d0..572b98f 100644 --- a/foundry/test/RewardEngine.t.sol +++ b/foundry/test/RewardEngine.t.sol @@ -19,6 +19,7 @@ import {DINModelRegistry} from "../src/DINModelRegistry.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract RewardEngineTest is Test { using stdStorage for StdStorage; @@ -218,11 +219,8 @@ contract RewardEngineTest is Test { (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { - bytes32 commitHash = keccak256( - abi.encodePacked(uint256(80), true, TEST_SALT) - ); vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } @@ -959,13 +957,10 @@ contract RewardEngineTest is Test { (, address[] memory b0Auditors, uint[] memory b0Models, ) = ta .getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256( - abi.encodePacked(uint256(80), true, TEST_SALT) - ); for (uint i = 0; i < b0Auditors.length; i++) { for (uint m = 0; m < b0Models.length; m++) { vm.prank(b0Auditors[i]); - ta.commitAuditScore(1, 0, b0Models[m], commitHash); + ta.commitAuditScore(1, 0, b0Models[m], auditCommitHash(uint256(80), true, TEST_SALT, b0Auditors[i], 1, 0, b0Models[m])); } } diff --git a/foundry/test/ScoringValidation.t.sol b/foundry/test/ScoringValidation.t.sol index 496c14f..1b4444d 100644 --- a/foundry/test/ScoringValidation.t.sol +++ b/foundry/test/ScoringValidation.t.sol @@ -18,6 +18,7 @@ import {DINModelRegistry} from "../src/DINModelRegistry.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract ScoringValidationTest is Test { DinToken tokenImpl; @@ -197,11 +198,8 @@ contract ScoringValidationTest is Test { uint256 score, bool vote ) internal { - bytes32 commitHash = keccak256( - abi.encodePacked(score, vote, TEST_SALT) - ); vm.prank(auditor); - ta.commitAuditScore(gi, batchId, modelIndex, commitHash); + ta.commitAuditScore(gi, batchId, modelIndex, auditCommitHash(score, vote, TEST_SALT, auditor, gi, batchId, modelIndex)); } function _revealScore( diff --git a/foundry/test/SecurityFindings.t.sol b/foundry/test/SecurityFindings.t.sol index 0cb5995..a36626f 100644 --- a/foundry/test/SecurityFindings.t.sol +++ b/foundry/test/SecurityFindings.t.sol @@ -34,6 +34,7 @@ import { TC_AuditSeedNotLocked, TA_AuditSeedNotLocked } from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract SecurityFindingsTest is Test { // ───────────────────────────────────────────────────────────────────── @@ -304,11 +305,10 @@ contract SecurityFindingsTest is Test { // score 100 on both models (commit-then-reveal per task_210726_6 §2a // replaced the old single-shot setAuditScorenEligibility). (, address[] memory batchAuditors, uint[] memory modelIdxs,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(100), true, TEST_SALT)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(100), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } @@ -446,11 +446,10 @@ contract SecurityFindingsTest is Test { // measure closeLMsubmissionsEvaluation's gas directly, matching what // this helper returned to callers before commit-reveal existed. (, address[] memory batch0Auditors, uint[] memory batch0Models,) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(100), true, TEST_SALT)); for (uint i = 0; i < batch0Auditors.length; i++) { for (uint m = 0; m < batch0Models.length; m++) { vm.prank(batch0Auditors[i]); - ta.commitAuditScore(1, 0, batch0Models[m], commitHash); + ta.commitAuditScore(1, 0, batch0Models[m], auditCommitHash(uint256(100), true, TEST_SALT, batch0Auditors[i], 1, 0, batch0Models[m])); } } @@ -912,11 +911,10 @@ contract SecurityFindingsTest is Test { vm.stopPrank(); (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } vm.prank(modelOwner); @@ -967,11 +965,10 @@ contract SecurityFindingsTest is Test { vm.stopPrank(); (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } vm.prank(modelOwner); @@ -1013,11 +1010,10 @@ contract SecurityFindingsTest is Test { vm.stopPrank(); (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } vm.prank(modelOwner); @@ -1051,11 +1047,10 @@ contract SecurityFindingsTest is Test { vm.stopPrank(); (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); - bytes32 commitHash = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[i], 1, 0, modelIdxs[m])); } } vm.prank(modelOwner); diff --git a/foundry/test/StakingEnforcement.t.sol b/foundry/test/StakingEnforcement.t.sol index 85a8fbe..b86b852 100644 --- a/foundry/test/StakingEnforcement.t.sol +++ b/foundry/test/StakingEnforcement.t.sol @@ -24,6 +24,7 @@ import { TA_StakeBelowModelFloor, TA_ConcurrentRegistrationCapReached } from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract StakingEnforcementTest is Test { DinToken token; @@ -365,9 +366,8 @@ contract StakingEnforcementTest is Test { bytes32 salt = bytes32(uint256(0xC0FFEE)); for (uint i = 0; i < batchAuditors.length; i++) { for (uint m = 0; m < modelIdxs.length; m++) { - bytes32 commitHash = keccak256(abi.encodePacked(uint256(80), true, salt)); vm.prank(batchAuditors[i]); - ta.commitAuditScore(1, 0, modelIdxs[m], commitHash); + ta.commitAuditScore(1, 0, modelIdxs[m], auditCommitHash(uint256(80), true, salt, batchAuditors[i], 1, 0, modelIdxs[m])); } } diff --git a/foundry/test/TreasuryForwarding.t.sol b/foundry/test/TreasuryForwarding.t.sol index c9a3c52..f3256b1 100644 --- a/foundry/test/TreasuryForwarding.t.sol +++ b/foundry/test/TreasuryForwarding.t.sol @@ -21,6 +21,7 @@ import {DINModelRegistry} from "../src/DINModelRegistry.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; +import {auditCommitHash} from "./utils/AuditCommitHash.sol"; contract TreasuryForwardingTest is Test { DinToken token; @@ -180,9 +181,8 @@ contract TreasuryForwardingTest is Test { (, address[] memory batchAuditors, uint[] memory modelIdxs, ) = ta.getAuditorsBatch(1, 0); for (uint ai = 0; ai < batchAuditors.length; ai++) { for (uint mi = 0; mi < modelIdxs.length; mi++) { - bytes32 ch = keccak256(abi.encodePacked(uint256(80), true, TEST_SALT)); vm.prank(batchAuditors[ai]); - ta.commitAuditScore(1, 0, modelIdxs[mi], ch); + ta.commitAuditScore(1, 0, modelIdxs[mi], auditCommitHash(uint256(80), true, TEST_SALT, batchAuditors[ai], 1, 0, modelIdxs[mi])); } } diff --git a/foundry/test/utils/AuditCommitHash.sol b/foundry/test/utils/AuditCommitHash.sol new file mode 100644 index 0000000..a5eea2f --- /dev/null +++ b/foundry/test/utils/AuditCommitHash.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +/// @dev The DINTaskAuditor commit hash, in one place for every test that +/// commits audit scores. Must match revealAuditScore exactly: it binds the +/// committing auditor and the (gi, batchId, modelIndex) slot (issue #192), +/// so a hash is valid for one auditor and one model only -- tests must +/// hash per auditor, not reuse one hash across an auditor loop. +function auditCommitHash( + uint256 score, + bool vote, + bytes32 salt, + address auditor, + uint256 gi, + uint256 batchId, + uint256 modelIndex +) pure returns (bytes32) { + return keccak256(abi.encode(score, vote, salt, auditor, gi, batchId, modelIndex)); +} diff --git a/tests/test_auditor_commit_hash.py b/tests/test_auditor_commit_hash.py new file mode 100644 index 0000000..d6cb538 --- /dev/null +++ b/tests/test_auditor_commit_hash.py @@ -0,0 +1,51 @@ +"""Tests for dincli.cli.auditor._audit_commit_hash (issue #192). + +DINTaskAuditor.revealAuditScore requires commitHash == +keccak256(abi.encode(score, vote, salt, msg.sender, gi, batchId, modelIndex)). +This must match byte-for-byte, or every `auditor lms-evaluation reveal` would +revert with TA_RevealHashMismatch and the auditor be S1-slashed. Golden values +below were computed independently via `cast abi-encode` + `cast keccak` (not by +importing this module's own encoding logic), so a field-order or type mistake +here would be caught rather than silently agreeing with itself. +""" +from web3 import Web3 + +from dincli.cli.auditor import _audit_commit_hash + +SALT_AA = Web3.to_bytes(hexstr="0x00000000000000000000000000000000000000000000000000000000000000aa") +SENDER_A = "0x1111111111111111111111111111111111111111" +SENDER_B = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd" + + +def test_audit_commit_hash_matches_cast_golden_vector_eligible(): + result = _audit_commit_hash(80, True, SALT_AA, SENDER_A, 3, 1, 7) + + assert result.hex() == "a2683a5803068e12385e07c963a48b966dcfe2543d3b5c29915083d4ebc8c97c" + + +def test_audit_commit_hash_matches_cast_golden_vector_ineligible(): + salt = Web3.to_bytes(hexstr="0x000000000000000000000000000000000000000000000000000000000000dead") + + result = _audit_commit_hash(35, False, salt, SENDER_B, 7, 0, 2) + + assert result.hex() == "82fd3121412903eecc66c36416d5d850461e4304c1e7fbcd7bbed87c38a5a127" + + +def test_audit_commit_hash_changes_with_sender(): + """Binding msg.sender is the #192 fix: two auditors committing the same + (score, vote, salt) must get different hashes, or one could copy the + other's commit hash and replay its reveal.""" + hash_a = _audit_commit_hash(80, True, SALT_AA, SENDER_A, 3, 1, 7) + hash_b = _audit_commit_hash(80, True, SALT_AA, SENDER_B, 3, 1, 7) + + assert hash_a != hash_b + + +def test_audit_commit_hash_changes_with_slot(): + """gi, batchId and modelIndex are bound too, so one commit can't be + reused for another model, batch or GI.""" + base = _audit_commit_hash(80, True, SALT_AA, SENDER_A, 3, 1, 7) + + assert base != _audit_commit_hash(80, True, SALT_AA, SENDER_A, 4, 1, 7) + assert base != _audit_commit_hash(80, True, SALT_AA, SENDER_A, 3, 2, 7) + assert base != _audit_commit_hash(80, True, SALT_AA, SENDER_A, 3, 1, 8)