diff --git a/Developer/tasks/task_061026_21.md b/Developer/tasks/task_061026_21.md index 572f5498..6829372c 100644 --- a/Developer/tasks/task_061026_21.md +++ b/Developer/tasks/task_061026_21.md @@ -46,7 +46,7 @@ dincli and the full-GI integration suite never caught up: - batches created vs test data assigned - aggregation seed waiting vs locked - Worse, `startLMsubmissionsEvaluation` (`DINTaskCoordinator.sol:532-538`) **doesn't check** that test data was assigned. The auditor-side flag (`DINTaskAuditor.sol:1017-1029`) is one-way, but it's only the owner's own claim; nothing checks that every batch actually has test data. + Worse, `startLMsubmissionsEvaluation` (`DINTaskCoordinator.sol:532-538`) **doesn't check** that test data was assigned. The auditor-side flag (`DINTaskAuditor.sol:1033-1045`) is one-way, but it's only the owner's own claim; nothing checks that every batch actually has test data. - **The public GI docs are stale**: wrong command names, and the funding, key, seed and reveal steps are missing. ### Discussion #216 follow-ups @@ -86,11 +86,13 @@ This is a read-only pass over every owner and role GI function in `DINTaskCoordi |---|---|---| | `modelId` constructor argument on both task contracts | `DINTaskCoordinator.sol:290`, `DINTaskAuditor.sol:406` | Deploy fails at ABI encoding (BL-27) | | `setDinToken` on the auditor (and the coordinator, for dispute bonds) | `DINTaskAuditor.sol:440`, `DINTaskCoordinator.sol:1307` | `depositRewards`, claims and bonds can't move DIN | -| The GI's reward pool funded before each `startGI` | `DINTaskCoordinator.sol:379` → `DINTaskAuditor.depositRewards` (`:481`) or `DinEmission.fundGI` (`DinEmission.sol:174`) | `TC_GIRewardPoolNotFunded` | -| Every batch auditor has an X25519 key on `DinValidatorStake` | `registerEncryptionKey` (`DinValidatorStake.sol:501`), checked at `DINTaskAuditor.sol:1000-1001` | `TA_AuditorEncryptionKeyNotRegistered` when test data is assigned | -| Seed blocks get mined | `disputeSeedDelay` = 7 blocks; dincli polls (`utils.py:998-1080`); anvil `--block-time 2` mines | The seed lock hangs | +| The GI's reward pool funded before each `startGI` | `DINTaskCoordinator.sol:380` → `DINTaskAuditor.depositRewards` (`:481`) or `DinEmission.fundGI` (`DinEmission.sol:212`) | `TC_GIRewardPoolNotFunded` | +| Every batch auditor has an X25519 key on `DinValidatorStake` | `registerEncryptionKey` (`DinValidatorStake.sol:509`), checked at `DINTaskAuditor.sol:1018-1019` | `TA_AuditorEncryptionKeyNotRegistered` when test data is assigned | +| Seed blocks get mined | `disputeSeedDelay` = 7 blocks; dincli polls (`utils.py:1029-1143`); anvil `--block-time 2` mines | The seed lock hangs | | The owner opens each of the three reveal windows | `start*Reveal` | `close` / `finalize` revert | +**Re-checked at `eb8085d` (PR 1's base):** the preconditions hold, and line references in this spec are updated to that commit. The coverage table below is unchanged: every row marked **missing** is still missing. + **dincli coverage at `a1fcce2`:** | GI step | Contract function | dincli command | @@ -135,7 +137,7 @@ Each new state is **set by the function that already does the step**, so a GI ne | New state (lifecycle / enum order) | Inserted | Set by | Gate change | |---|---|---|---| | `AuditSeedLocked` | between `LMSclosed` and `AuditorsBatchesCreated` | `lockAuditSeed` (`DINTaskCoordinator.sol:1483`, anyone). Only on a successful lock, never on a re-anchor | `createAuditorsBatches` requires `AuditSeedLocked` | -| `AuditTestDataAssigned` | between `AuditorsBatchesCreated` and `LMSevaluationStarted` | `setTestDataAssignedFlag` (`:519`) | `startLMsubmissionsEvaluation` requires it. **The auditor side (`DINTaskAuditor.sol:1017-1029`) checks that every batch of the GI has a stored commitment** (`testDataCommitments[gi][b] != 0`, written by `assignAuditTestDataset`), else it reverts with a new `TA_TestDataNotAssigned`. The flag is already one-way (`TA_FlagMustBeTrue`, `TA_FlagAlreadySet`) | +| `AuditTestDataAssigned` | between `AuditorsBatchesCreated` and `LMSevaluationStarted` | `setTestDataAssignedFlag` (`:519`) | `startLMsubmissionsEvaluation` requires it. **The auditor side (`DINTaskAuditor.sol:1033-1045`) checks that every batch of the GI has a stored commitment** (`testDataCommitments[gi][b] != 0`, written by `assignAuditTestDataset`), else it reverts with a new `TA_TestDataNotAssigned`. The flag is already one-way (`TA_FlagMustBeTrue`, `TA_FlagAlreadySet`) | | `AggSeedLocked` | between `LMSevaluationClosed` and `T1nT2Bcreated` | `lockAggSeed` (`:1459`) | `autoCreateTier1AndTier2` requires `AggSeedLocked` | **Not added**, and why: @@ -196,7 +198,7 @@ Each new state is **set by the function that already does the step**, so a GI ne | `dincli rewards claim --gi N` / `dincli rewards withdraw ` | `claimReward(gi)` / `claimRewards()` | All roles. First checks `giRewardSnapshot(gi).settled` and `rewardClaimed` | | `dincli auditor register-encryption-key` | `DinValidatorStake.registerEncryptionKey` | Generates the X25519 key **per wallet** (e.g. `auditor_x25519_
.key`, chmod 600), replacing today's single shared `auditor_x25519.key`, which breaks several `--demokey` auditors on one machine. Updates `_load_auditor_x25519_key` (`auditor.py:33-41`) to match. The owner key (`auditor_batches.py:170-177`) also gets chmod 600 | | `dincli model-owner gi release-slots --gi N` | `releaseGIRegistrationSlots` | BL-28 | -| `dincli auditor dispute-test-data`, `dincli model-owner disputes resolve-test-data / reassign-test-data`, `… disputes close-expired` | `openTestDataDispute` (`DINTaskAuditor.sol:1480`), `resolveTestDataDispute` (`:1524`), `closeExpiredDispute` (`:1557`), `reassignAuditTestDataset` (`:1601`) | **Decision 4: in this task.** Built against the PR #215 shape: only batch auditors open a dispute, only the owner resolves, an unanswered dispute is upheld on expiry, there's no penalty after settlement, and `TA_RewardsAlreadySettled` applies. Bond approval comes first | +| `dincli auditor dispute-test-data`, `dincli model-owner disputes resolve-test-data / reassign-test-data`, `… disputes close-expired` | `openTestDataDispute` (`DINTaskAuditor.sol:1496`), `resolveTestDataDispute` (`:1540`), `closeExpiredDispute` (`:1573`), `reassignAuditTestDataset` (`:1617`) | **Decision 4: in this task.** Built against the PR #215 shape: only batch auditors open a dispute, only the owner resolves, an unanswered dispute is upheld on expiry, there's no penalty after settlement, and `TA_RewardsAlreadySettled` applies. Bond approval comes first | | `dincli aggregator dispute`, `dincli model-owner disputes resolve-aggregation / settle-recomputation`, `… expire`, `… claim-bond` | `openDispute` (`DINTaskCoordinator.sol:1368`), `resolveDispute` (`:1535`), `settleRecomputation` (`:1609`), `expireDispute` (`:1671`), `claimDisputeBond` (`:1583`) | S4 | **Fail fast instead of reverting:** @@ -204,7 +206,7 @@ Each new state is **set by the function that already does the step**, so a GI ne - `create-testdataset --submit` checks every batch auditor's key and names any that are missing. **Reuse:** -- `build_and_send_tx` (`dincli/cli/utils.py:947`) +- `build_and_send_tx` (`dincli/cli/utils.py:978`) - the `ctx.obj.get_deployed_din_task_*_contract(model_id)` lookups (`cli/context.py:339`, `:357`) - a new `approve_din(ctx, spender, amount)` helper, taken from the inline approve in `dintoken.stake_dintokens` (`dincli/cli/dintoken.py:71-128`) diff --git a/Documentation/technical/contracts/DINShared.md b/Documentation/technical/contracts/DINShared.md index 55e74c15..d2a74807 100644 --- a/Documentation/technical/contracts/DINShared.md +++ b/Documentation/technical/contracts/DINShared.md @@ -38,24 +38,27 @@ Value State Name Description 8 DINauditorsRegistrationStarted Auditor registration window is open. 9 DINauditorsRegistrationClosed Auditor registration window is closed. 10 LMSstarted Local Model Submission window is open. - 11 LMSclosed Local Model Submission window is closed. - 12 AuditorsBatchesCreated Audit batches have been formed. - 13 LMSevaluationStarted Commit phase: auditors submit hidden score/vote commitments via `commitAuditScore`. - 14 LMSevaluationRevealStarted Reveal phase: auditors reveal (score, vote, salt) via `revealAuditScore`; eligibility and median scoring are computed from revealed values only. - 15 LMSevaluationClosed Evaluation finalized; approved models identified. - 16 T1nT2Bcreated Tier-1 and Tier-2 aggregation batches formed. - 17 T1AggregationStarted Commit phase: Tier-1 aggregators submit hidden CID commitments via `commitT1Aggregation`. - 18 T1AggregationRevealStarted Reveal phase: Tier-1 aggregators reveal (cid, salt) via `revealT1Aggregation`; only revealed CIDs count toward finalization. - 19 T1AggregationDone Tier-1 finalized; winning CIDs per batch recorded. - 20 T2AggregationStarted Commit phase: Tier-2 aggregators submit hidden CID commitments via `commitT2Aggregation`. - 21 T2AggregationRevealStarted Reveal phase: Tier-2 aggregators reveal (cid, salt) via `revealT2Aggregation`; only revealed CIDs count toward finalization. - 22 T2AggregationDone Tier-2 finalized; global winning CID recorded. - 23 AuditorsSlashed Auditor slashing phase executed. - 24 AggregatorsSlashed Aggregator slashing phase executed. - 25 GIended GI is complete; system is ready for next GI. + 11 LMSclosed Local Model Submission window is closed; audit seed block anchored. + 12 AuditSeedLocked Audit batch seed locked (`lockAuditSeed`, permissionless). + 13 AuditorsBatchesCreated Audit batches have been formed. + 14 AuditTestDataAssigned Every audit batch has a test-data commitment; `setTestDataAssignedFlag` done. + 15 LMSevaluationStarted Commit phase: auditors submit hidden score/vote commitments via `commitAuditScore`. + 16 LMSevaluationRevealStarted Reveal phase: auditors reveal (score, vote, salt) via `revealAuditScore`; eligibility and median scoring are computed from revealed values only. + 17 LMSevaluationClosed Evaluation finalized; approved models identified; aggregation seed block anchored. + 18 AggSeedLocked T1/T2 batch seed locked (`lockAggSeed`, permissionless). + 19 T1nT2Bcreated Tier-1 and Tier-2 aggregation batches formed. + 20 T1AggregationStarted Commit phase: Tier-1 aggregators submit hidden CID commitments via `commitT1Aggregation`. + 21 T1AggregationRevealStarted Reveal phase: Tier-1 aggregators reveal (cid, salt) via `revealT1Aggregation`; only revealed CIDs count toward finalization. + 22 T1AggregationDone Tier-1 finalized; winning CIDs per batch recorded. + 23 T2AggregationStarted Commit phase: Tier-2 aggregators submit hidden CID commitments via `commitT2Aggregation`. + 24 T2AggregationRevealStarted Reveal phase: Tier-2 aggregators reveal (cid, salt) via `revealT2Aggregation`; only revealed CIDs count toward finalization. + 25 T2AggregationDone Tier-2 finalized; global winning CID recorded. + 26 AuditorsSlashed Auditor slashing phase executed. + 27 AggregatorsSlashed Aggregator slashing phase executed. + 28 GIended GI is complete; system is ready for next GI. ``` -> **Ordinal note:** `LMSevaluationRevealStarted` (commit-then-reveal auditor scoring, task_210726_6 §2a) sits between `LMSevaluationStarted` and `LMSevaluationClosed` at ordinal 14. `T1AggregationRevealStarted` and `T2AggregationRevealStarted` (commit-then-reveal T1/T2 aggregation, issue #156 M-1, task_240926_18 Part C) sit at ordinals 18 and 21 respectively, immediately after their corresponding commit-phase state — same insert-in-lifecycle-position precedent, not appended. Each insertion shifts every later ordinal by +1 relative to the prior numbering. `dincli/cli/utils.py`'s `states`/`stateDescription` positional mirrors (indexed by this same raw ordinal) have been updated to match — see `dincli/cli/utils.py`'s `states`/`stateDescription` lists. +> **Ordinal note:** `LMSevaluationRevealStarted` (commit-then-reveal auditor scoring, task_210726_6 §2a) sits between `LMSevaluationStarted` and `LMSevaluationClosed` at ordinal 16. `T1AggregationRevealStarted` and `T2AggregationRevealStarted` (commit-then-reveal T1/T2 aggregation, issue #156 M-1, task_240926_18 Part C) sit at ordinals 21 and 24 respectively, immediately after their corresponding commit-phase state — same insert-in-lifecycle-position precedent, not appended. Each insertion shifts every later ordinal by +1 relative to the prior numbering. The three progress states `AuditSeedLocked` (12), `AuditTestDataAssigned` (14) and `AggSeedLocked` (18) (task_061026_21 Part B) follow the same precedent; with them `GIended` moved from 25 to 28. Each is set by the function that already does the step, so a GI needs no extra owner transaction. `dincli/cli/utils.py`'s `states`/`stateDescription` positional mirrors (indexed by this same raw ordinal) have been updated to match — see `dincli/cli/utils.py`'s `states`/`stateDescription` lists. ### 2.2 State Transition Diagram @@ -72,41 +75,44 @@ Value State Name Description [3] AwaitingGenesisModel │ setGenesisModelIpfsHash() ▼ -[4] GenesisModelCreated ◄──────────────────────────────── [25] GIended - │ startGI() ▲ - ▼ │ endGI() -[5] GIstarted [24] AggregatorsSlashed - │ startDINaggregatorsRegistration() ▲ - ▼ │ slashAggregators() -[6] DINaggregatorsRegistrationStarted [23] AuditorsSlashed - │ closeDINaggregatorsRegistration() ▲ - ▼ │ slashAuditors() -[7] DINaggregatorsRegistrationClosed [22] T2AggregationDone - │ startDINauditorsRegistration() ▲ - ▼ │ finalizeT2Aggregation() -[8] DINauditorsRegistrationStarted [21] T2AggregationRevealStarted - │ closeDINauditorsRegistration() ▲ - ▼ │ startT2AggregationReveal() -[9] DINauditorsRegistrationClosed [20] T2AggregationStarted - │ startLMsubmissions() ▲ - ▼ │ startT2Aggregation() -[10] LMSstarted [19] T1AggregationDone - │ closeLMsubmissions() ▲ - ▼ │ finalizeT1Aggregation() -[11] LMSclosed [18] T1AggregationRevealStarted - │ createAuditorsBatches() ▲ - ▼ │ startT1AggregationReveal() -[12] AuditorsBatchesCreated [17] T1AggregationStarted - │ startLMsubmissionsEvaluation() ▲ - ▼ │ startT1Aggregation() -[13] LMSevaluationStarted [16] T1nT2Bcreated - │ (auditors: commitAuditScore, commit phase) ▲ - └──────────── startLMsubmissionsEvaluationReveal() ────────┤ autoCreateTier1AndTier2() - (auditors: revealAuditScore, reveal phase) │ - [15] LMSevaluationClosed - ▲ - │ closeLMsubmissionsEvaluation() - [14] LMSevaluationRevealStarted +[4] GenesisModelCreated ◄───────────────────────────[28] GIended + │ startGI() ▲ + ▼ │ endGI() +[5] GIstarted [27] AggregatorsSlashed + │ startDINaggregatorsRegistration() ▲ + ▼ │ slashAggregators() +[6] DINaggregatorsRegistrationStarted [26] AuditorsSlashed + │ closeDINaggregatorsRegistration() ▲ + ▼ │ slashAuditors() +[7] DINaggregatorsRegistrationClosed [25] T2AggregationDone + │ startDINauditorsRegistration() ▲ + ▼ │ finalizeT2Aggregation() +[8] DINauditorsRegistrationStarted [24] T2AggregationRevealStarted + │ closeDINauditorsRegistration() ▲ + ▼ │ startT2AggregationReveal() +[9] DINauditorsRegistrationClosed [23] T2AggregationStarted + │ startLMsubmissions() ▲ + ▼ │ startT2Aggregation() +[10] LMSstarted [22] T1AggregationDone + │ closeLMsubmissions() ▲ + ▼ │ finalizeT1Aggregation() +[11] LMSclosed [21] T1AggregationRevealStarted + │ lockAuditSeed() (anyone) ▲ + ▼ │ startT1AggregationReveal() +[12] AuditSeedLocked [20] T1AggregationStarted + │ createAuditorsBatches() ▲ + ▼ │ startT1Aggregation() +[13] AuditorsBatchesCreated [19] T1nT2Bcreated + │ setTestDataAssignedFlag() ▲ + ▼ │ autoCreateTier1AndTier2() +[14] AuditTestDataAssigned [18] AggSeedLocked + │ startLMsubmissionsEvaluation() ▲ + ▼ │ lockAggSeed() (anyone) +[15] LMSevaluationStarted [17] LMSevaluationClosed + │ (auditors: commitAuditScore) ▲ + │ │ closeLMsubmissionsEvaluation() + └── startLMsubmissionsEvaluationReveal() ─► [16] LMSevaluationRevealStarted + (auditors: revealAuditScore) ``` T1/T2 aggregation submissions follow the same commit-then-reveal shape as LMS evaluation above: `commitT1Aggregation`/`commitT2Aggregation` during the `*AggregationStarted` (commit) state, then the model owner calls `startT1AggregationReveal`/`startT2AggregationReveal` to open `*AggregationRevealStarted`, during which aggregators call `revealT1Aggregation`/`revealT2Aggregation`. Only revealed CIDs are counted by `finalizeT1Aggregation`/`finalizeT2Aggregation`; a committed-but-never-revealed aggregator is excluded from finalization and remains slashable via `slashAggregators`' existing "no submission" (S2) check (issue #156 M-1, task_240926_18 Part C). @@ -192,7 +198,7 @@ Used by: `DINTaskCoordinator` | Method | Purpose | |--------|---------| -| `createAuditorsBatches` | Batch formation, triggered by the coordinator in `LMSclosed`. `seed` is the coordinator's locked `auditSeed[_GI]`; a zero seed reverts `TA_AuditSeedNotLocked` | +| `createAuditorsBatches` | Batch formation, triggered by the coordinator in `AuditSeedLocked`. `seed` is the coordinator's locked `auditSeed[_GI]`; a zero seed reverts `TA_AuditSeedNotLocked` | | `setTestDataAssignedFlag` | Records that test datasets were distributed (informational — nothing gates on it) | | `finalizeEvaluation` | Median scores + approval; requires `LMSevaluationRevealStarted` | | `slashAuditors` | S1/S3 auditor slashing; requires `T2AggregationDone` | @@ -234,12 +240,12 @@ Used by: `DINTaskCoordinator` | `TA_AlreadySubmitted` | Client has already submitted a model this GI | | `TA_MaxLMSubmissionsReached` | Submission count reached `MAX_LM_SUBMISSIONS` (10,000) | | `TA_NotEnoughAuditors` | Too few auditors to form even one batch | -| `TA_CannotCreateAuditorsBatches` | State is not `LMSclosed` | +| `TA_CannotCreateAuditorsBatches` | State is not `AuditSeedLocked` | | `TA_BatchDoesNotExist` | Batch ID >= batch array length | | `TA_BatchIDMismatch` | Internal sanity check failure on batch ID | | `TA_CannotSetTestDataAssignedFlag` | State is not `AuditorsBatchesCreated` | | `TA_FlagMustBeTrue` | `setTestDataAssignedFlag` called with `flag = false` | -| `TA_FlagAlreadySet` | Flag was already set for this GI | +| `TA_TestDataNotAssigned` | `setTestDataAssignedFlag`: some audit batch of the GI has no stored test-data commitment (task_061026_21 Part B). A second call is rejected by the state gate (`TA_CannotSetTestDataAssignedFlag` / `TC_CannotSetTestDataAssignedFlag`), so `TA_FlagAlreadySet` is gone | | `TA_NotAssignedAuditor` | Score commit/reveal, or `openTestDataDispute`, from an auditor not assigned to the batch | | `TA_InvalidModelIndex` | Model index not assigned to this batch | | `TA_CannotSetAuditScore` | Declared but unused — left over from the pre-commit-reveal `setAuditScorenEligibility`. | @@ -279,11 +285,12 @@ Used by: `DINTaskCoordinator` | `TC_AuditorsRegistrationCannotBeFinished` | Auditor registration close in wrong state | | `TC_LMSubmissionsCannotBeStarted` | LM submission window start in wrong state | | `TC_LMSubmissionsNotStarted` | LM submission close when not started | -| `TC_LMEvalCannotBeStarted` | Reused by two functions: `createAuditorsBatches` when state is not `LMSclosed`, and `startLMsubmissionsEvaluation` (opens the commit phase) when state is not `AuditorsBatchesCreated` | +| `TC_LMEvalCannotBeStarted` | `startLMsubmissionsEvaluation` (opens the commit phase) when state is not `AuditTestDataAssigned` | | `TC_LMEvalCannotBeFinished` | `closeLMsubmissionsEvaluation` called when state is not `LMSevaluationRevealStarted` | | `TC_FailedToCreateAuditorsBatches` | `createAuditorsBatches` returned false | | `TC_CannotSetTestDataAssignedFlag` | Test data flag set in wrong state | -| `TC_EvalPhaseNotClosed` | T1/T2 batch creation before evaluation close | +| `TC_GINotEnded` | `releaseGIRegistrationSlots` for a GI that hasn't reached `GIended` | +| `TC_SlotsAlreadyReleased` | `releaseGIRegistrationSlots` called twice for the same GI | | `TC_NotEnoughValidators` | Too few aggregators for T1 batches | | `TC_NotEnoughApprovedModels` | Fewer than `T1_MODELS_PER_BATCH` models approved | | `TC_BatchNotFound` | `getTier1Batch` with a batch ID out of bounds | @@ -366,11 +373,11 @@ One set per seed, mirroring the dispute-seed errors in §4.6. See [DINTaskCoordi | `TC_AggSeedNotAnchored` | `lockAggSeed` called before `aggSeedBlock[_GI]` has been set (i.e. before `closeLMsubmissionsEvaluation`) | | `TC_AggSeedBlockNotMined` | `lockAggSeed` called before `block.number > aggSeedBlock[_GI]` | | `TC_AggSeedAlreadyLocked` | `lockAggSeed` called when `aggSeed[_GI]` is already non-zero | -| `TC_AggSeedNotLocked` | `autoCreateTier1AndTier2` called while `aggSeed[_GI] == bytes32(0)` | +| `TC_AggSeedNotLocked` | `autoCreateTier1AndTier2` called when state is not `AggSeedLocked` (the seed isn't locked yet, or the GI is elsewhere) | | `TC_AuditSeedNotAnchored` | `lockAuditSeed` called before `auditSeedBlock[_GI]` has been set (i.e. before `closeLMsubmissions`) | | `TC_AuditSeedBlockNotMined` | `lockAuditSeed` called before `block.number > auditSeedBlock[_GI]` | | `TC_AuditSeedAlreadyLocked` | `lockAuditSeed` called when `auditSeed[_GI]` is already non-zero | -| `TC_AuditSeedNotLocked` | `createAuditorsBatches` called while `auditSeed[_GI] == bytes32(0)` | +| `TC_AuditSeedNotLocked` | `createAuditorsBatches` called when state is not `AuditSeedLocked` (the seed isn't locked yet, or the GI is elsewhere) | | `TA_AuditSeedNotLocked` | `DINTaskAuditor.createAuditorsBatches(uint, bytes32)` rejects a zero seed itself, as a second guard behind the coordinator's `TC_AuditSeedNotLocked` check | ### 4.8 Commit-then-reveal T1/T2 aggregation (`TC_*`, issue #156 M-1) diff --git a/Documentation/technical/contracts/DINTaskAuditor.md b/Documentation/technical/contracts/DINTaskAuditor.md index 1b6b5757..e32bbfe5 100644 --- a/Documentation/technical/contracts/DINTaskAuditor.md +++ b/Documentation/technical/contracts/DINTaskAuditor.md @@ -83,7 +83,7 @@ Mappings marked *(internal)* have no auto-getter; read them through the view nam - **Submissions:** `lmSubmissions[gi]` (`LMSubmission`: client, modelCID, submittedAt, eligible, evaluated, approved, finalMedianScore), `clientHasSubmitted`, `clientSubmissionIndex`. - **Batches:** `auditBatches[gi]` *(internal; read with `getAuditorsBatch(gi, batchId)`)* (`AuditBatch`: batchId, auditors, modelIndexes, testDataCID), `isBatchAuditor`, `isBatchModelIndex`. - **Scoring:** `auditScoreCommits`, `hasCommittedLM`, `auditScores`, `LMeligibleVote`, `hasAuditedLM` (all keyed `[gi][batchId][auditor][modelIndex]`). -- **Test data:** `encryptedTestDataKey[gi][batchId][auditor]`, `testDataCommitments[gi][batchId]`, `Is_testdataCIDs_Assigned[gi]` *(internal; only guards `setTestDataAssignedFlag` against a second call)*, `testDataDisputes[gi][batchId]`. +- **Test data:** `encryptedTestDataKey[gi][batchId][auditor]`, `testDataCommitments[gi][batchId]`, `testDataDisputes[gi][batchId]`. - **Rewards:** `giRewardPool[gi]`, `giRewardSnapshot[gi]`, `giTotalApprovedScore[gi]`, `giTotalAuditWeight[gi]`, `auditorGIWeight[gi][addr]` *(internal; feeds `claimReward`)*, `rewardClaimed[gi][addr]`, `claimable[addr]`, `treasuryAccrued`. --- @@ -110,14 +110,14 @@ Mappings marked *(internal)* have no auto-getter; read them through the view nam ## 6. Batches & Test Data -- **`createAuditorsBatches(gi, seed)`** (from the coordinator, state `LMSclosed`) — `seed` is the coordinator's locked `auditSeed[gi]` (see [DINTaskCoordinator §6.3](DINTaskCoordinator.md#63-batch-assignment-seed-lock)). A zero seed reverts with `TA_AuditSeedNotLocked`; the coordinator checks this too, so this is a second guard. The function then: +- **`createAuditorsBatches(gi, seed)`** (from the coordinator, state `AuditSeedLocked`) — `seed` is the coordinator's locked `auditSeed[gi]` (see [DINTaskCoordinator §6.3](DINTaskCoordinator.md#63-batch-assignment-seed-lock)). A zero seed reverts with `TA_AuditSeedNotLocked`; the coordinator checks this too, so this is a second guard. The function then: 1. filters `dinAuditors[gi]` to auditors still `isValidatorActive` at call time (`TA_NotEnoughAuditors` if fewer than `auditorsPerBatch` remain); 2. shuffles the active auditors with `keccak256(seed, "AUD_ADDR")` and the model indexes with `keccak256(seed, "AUD_IDX")` (Fisher-Yates, in memory); 3. greedily forms batches of 3 auditors × 3 models (the last batch may take 2). Leftover auditors and models are unused. Emits `AuditorsBatchAuto` per batch and `AuditorsBatchesCreated`. The seed comes from a block hash, so the residual trust assumptions in the coordinator doc apply here as well; one of them is specific to this function: a validator can still change the active pool by unstaking after the seed is locked and before this call. - **`assignAuditTestDataset(gi, batchId, testDataCID, encryptedKeys[], commitment)`** (owner) — stores the encrypted test-data CID (`AES-256-GCM(K, rawCID ‖ Sign(ownerSK, rawCID))`) and one encrypted copy of `K` per auditor, in the batch's auditor order. Every auditor must have an X25519 key on `DinValidatorStake`. Also stores `commitment = keccak256(gi, batchId, keccak256(K), keccak256(plaintext))`. Blocked while the batch awaits reassignment. -- **`setTestDataAssignedFlag(gi, true)`** (from the coordinator, state `AuditorsBatchesCreated`) — one-shot flag. Informational only: nothing checks it before scoring starts. +- **`setTestDataAssignedFlag(gi, true)`** (from the coordinator, state `AuditorsBatchesCreated`) — reverts `TA_TestDataNotAssigned` unless every batch of the GI has a stored `testDataCommitments` entry. On success the coordinator moves to `AuditTestDataAssigned`, which `startLMsubmissionsEvaluation` requires; leaving `AuditorsBatchesCreated` is what makes the call one-shot. --- @@ -204,7 +204,7 @@ Read alongside the [foundry/src security review](../audits/foundry-src-security- - **No. 4 — Unclaimable remainders.** Two distinct cases, both accepted by design (issue No. 250): - *Zero-participant case:* if no model is approved (`giTotalApprovedScore == 0`), or nobody reveals, or no aggregator weight exists, that role's entire pool share stays in the contract with no reclaim path. - *Per-participant rounding case:* `claimReward` floor-divides each role's pool proportionally per participant (`clientPool * finalMedianScore / totalApproved`, and the equivalent for `auditorPool`/`aggregatorPool`). Each participant's share is rounded down independently, so up to `participants − 1` wei per role bucket per GI is left behind, untracked (no "distributed so far" counter exists, and `claimReward` emits no per-GI event — `RewardsClaimed` in `claimRewards` carries no `gi` — so the per-GI total can only be recomputed off-chain from state) and unrecoverable. Distinct from, and not fixed by, [BL-14](../../../Developer/BACK_LOG.md) — BL-14's fix only swept the *top-level* 4-way bps split's remainder to treasury in `settleRewards`; it never touched this second, per-participant layer. Economically negligible at 18 decimals (bounded by floor-division error), but worth naming explicitly: a sweep would need a per-bucket participant-count tracked incrementally (the BL-10 refactor deliberately removed a settlement-time loop that could have supplied one), and [MECHANISM_DESIGN.md](../../../Developer/design/MECHANISM_DESIGN.md)'s "Unclaimed rewards: claimable indefinitely; no expiry for 2.0" rules out a deadline-based sweep anyway — so this is documented as permanent, accepted dust rather than fixed in code. -- **No. 5 — `setTestDataAssignedFlag` gates nothing:** scoring can open before any test data is assigned. +- **No. 5 — Fixed: scoring can't open before test data is assigned.** `setTestDataAssignedFlag` now checks every batch has a commitment and moves the coordinator to `AuditTestDataAssigned`, which `startLMsubmissionsEvaluation` requires (task_061026_21 Part B). - **No. 6 — Stale NatSpec and reused errors:** `slashAuditors` says S1 and S3 are both `minStake()` (S1 is now a fraction). Several comments call parameters "DAO-settable"; they are `onlyOwner`, i.e. set by the model owner. `setDisputePenaltyBps` reuses `TA_InvalidDisputeBond`, and `closeExpiredDispute` reuses `TA_DisputeWindowClosed` for "window still open". - **No. 7 — `modelId` is fixed at construction**, before the registry assigns it (see [DINTaskCoordinator §10 No. 4](DINTaskCoordinator.md#10-review-notes--open-caveats)). - **No. 8 — dincli lags this contract:** `dincli model-owner deploy task-auditor` still calls the older two-argument constructor (no `modelId`). @@ -227,3 +227,4 @@ Read alongside the [foundry/src security review](../audits/foundry-src-security- - Test-data disputes (issue No. 205): only an auditor of the batch can open one; `resolveTestDataDispute` is owner-only; `closeExpiredDispute` now upholds an unanswered dispute instead of forfeiting the bond; `disputeBondAmount` defaults to 100 DIN. `DisputeExpired` drops its `bondForfeited` field. Disputes can't be opened once the GI is settled (`TA_RewardsAlreadySettled`), and one upheld after settlement takes no pool penalty, so settled claims stay funded (PR No. 215 review). - Size review (issue No. 201 Part A item 4): `auditBatches`, `dinAuditors`, `Is_testdataCIDs_Assigned` and `auditorGIWeight` lose their auto-getters (nothing outside the contract read them; `getAuditorsBatch` / `getDINtaskAuditors` cover the first two). `assignAuditTestDataset` and `reassignAuditTestDataset` share `_storeTestData` and `_batchAt`, and `_burnAndForward` forwards its half through `_forwardToTreasury`. Behaviour and events are unchanged; runtime size drops from 22,718 B to 21,188 B. - Dual-role guard (issue No. 180): `registerDINAuditor` reverts with `TA_DualRoleNotAllowed` when the caller is already an aggregator for the same GI. The check is on the auditor side because aggregators always register first; it is per address, so a second address with its own stake can still take the auditor role. `IDINTaskCoordinator` gains `isDINAggregator`. +- Every-batch test-data check (task_061026_21 Part B): `setTestDataAssignedFlag` reverts `TA_TestDataNotAssigned` while any batch has no commitment; `createAuditorsBatches` now requires the coordinator's new `AuditSeedLocked` state. `Is_testdataCIDs_Assigned` and `TA_FlagAlreadySet` are removed, since the coordinator state now rejects a second call. diff --git a/Documentation/technical/contracts/DINTaskCoordinator.md b/Documentation/technical/contracts/DINTaskCoordinator.md index 65487cf4..1ded10f7 100644 --- a/Documentation/technical/contracts/DINTaskCoordinator.md +++ b/Documentation/technical/contracts/DINTaskCoordinator.md @@ -127,24 +127,24 @@ The numbers are `GIstates` ordinals (see [DINShared §2.1](DINShared.md)). | Step | Function (owner unless noted) | From → To state | |------|-------------------------------|-----------------| -| Start | `startGI(gi)` / `startGI(gi, passScore)` | `GenesisModelCreated` (4) or `GIended` (25) → `GIstarted` (5) | +| Start | `startGI(gi)` / `startGI(gi, passScore)` | `GenesisModelCreated` (4) or `GIended` (28) → `GIstarted` (5) | | Aggregator registration | `startDINaggregatorsRegistration` → *validators:* `registerDINaggregator` → `closeDINaggregatorsRegistration` | → 6 → 7 | | Auditor registration | `startDINauditorsRegistration` → *validators on the auditor contract* → `closeDINauditorsRegistration` | → 8 → 9 | | Local model submissions | `startLMsubmissions` → *clients on the auditor contract* → `closeLMsubmissions` (anchors the audit seed) | → 10 → 11 | -| Audit seed | *anyone:* `lockAuditSeed` | stays 11 | -| Audit batches | `createAuditorsBatches` (delegates to the auditor, passing the seed) | → 12 | -| Test data | `DINTaskAuditor.assignAuditTestDataset` per batch; `setTestDataAssignedFlag` | stays 12 | -| Score commit | `startLMsubmissionsEvaluation` → *auditors commit on the auditor contract* | → 13 | -| Score reveal | `startLMsubmissionsEvaluationReveal` → *auditors reveal* | → 14 | -| Finalize evaluation | `closeLMsubmissionsEvaluation` (calls `finalizeEvaluation`; anchors the aggregation seed) | → 15 | -| Aggregation seed | *anyone:* `lockAggSeed` | stays 15 | -| Batches | `autoCreateTier1AndTier2` | → 16 | -| T1 commit | `startT1Aggregation` → *aggregators:* `commitT1Aggregation` | → 17 | -| T1 reveal | `startT1AggregationReveal` → *aggregators:* `revealT1Aggregation` → `finalizeT1Aggregation` | → 18 → 19 | -| T2 commit | `startT2Aggregation` → *aggregators:* `commitT2Aggregation` | → 20 | -| T2 reveal | `startT2AggregationReveal` → *aggregators:* `revealT2Aggregation` → `finalizeT2Aggregation` | → 21 → 22 | -| Slash | `slashAuditors` → `slashAggregators` | → 23 → 24 | -| End | `endGI` (settles rewards) | → 25 | +| Audit seed | *anyone:* `lockAuditSeed` (a re-anchor leaves the state unchanged) | → 12 | +| Audit batches | `createAuditorsBatches` (delegates to the auditor, passing the seed) | → 13 | +| Test data | `DINTaskAuditor.assignAuditTestDataset` per batch; then `setTestDataAssignedFlag` (the auditor checks every batch has a commitment) | → 14 | +| Score commit | `startLMsubmissionsEvaluation` → *auditors commit on the auditor contract* | → 15 | +| Score reveal | `startLMsubmissionsEvaluationReveal` → *auditors reveal* | → 16 | +| Finalize evaluation | `closeLMsubmissionsEvaluation` (calls `finalizeEvaluation`; anchors the aggregation seed) | → 17 | +| Aggregation seed | *anyone:* `lockAggSeed` (a re-anchor leaves the state unchanged) | → 18 | +| Batches | `autoCreateTier1AndTier2` | → 19 | +| T1 commit | `startT1Aggregation` → *aggregators:* `commitT1Aggregation` | → 20 | +| T1 reveal | `startT1AggregationReveal` → *aggregators:* `revealT1Aggregation` → `finalizeT1Aggregation` | → 21 → 22 | +| T2 commit | `startT2Aggregation` → *aggregators:* `commitT2Aggregation` | → 23 | +| T2 reveal | `startT2AggregationReveal` → *aggregators:* `revealT2Aggregation` → `finalizeT2Aggregation` | → 24 → 25 | +| Slash | `slashAuditors` → `slashAggregators` | → 26 → 27 | +| End | `endGI` (settles rewards) | → 28 | | Housekeeping | `releaseGIRegistrationSlots(gi)` | no state change | Every owner transition after `startGI` takes the expected `_GI` and reverts `TC_WrongGI` if it is not the current one. Every state change goes through `_setGIstate`, which emits `GIStateChanged(GI, newState)`. @@ -166,7 +166,8 @@ Both batch-creation steps shuffle with a seed that must be locked first (issue # | Anchor | `closeLMsubmissions` sets `auditSeedBlock[gi] = block.number + disputeSeedDelay` | `closeLMsubmissionsEvaluation` sets `aggSeedBlock[gi]` the same way | | Lock (anyone, current GI) | `lockAuditSeed(gi)` | `lockAggSeed(gi)` | | Seed | `keccak256(blockhash(seedBlock), gi, "AUD")` | `keccak256(blockhash(seedBlock), gi, "AGG")` | -| Consumer | `createAuditorsBatches` (`TC_AuditSeedNotLocked` if unset; the auditor contract also rejects a zero seed with `TA_AuditSeedNotLocked`) | `autoCreateTier1AndTier2` (`TC_AggSeedNotLocked` if unset) | +| State on lock | `LMSclosed` → `AuditSeedLocked` | `LMSevaluationClosed` → `AggSeedLocked` | +| Consumer | `createAuditorsBatches`, gated on `AuditSeedLocked` (`TC_AuditSeedNotLocked` otherwise; the auditor contract also rejects a zero seed with `TA_AuditSeedNotLocked`) | `autoCreateTier1AndTier2`, gated on `AggSeedLocked` (`TC_AggSeedNotLocked` otherwise) | | Events | `AuditSeedLocked`, `AuditSeedReanchored` | `AggSeedLocked`, `AggSeedReanchored` | A lock call reverts if the seed isn't anchored yet (`TC_*SeedNotAnchored`), is already locked (`TC_*SeedAlreadyLocked`), or the anchor block isn't mined yet (`TC_*SeedBlockNotMined`). If more than 256 blocks have passed and `blockhash(seedBlock)` is zero, the call re-anchors to `block.number + disputeSeedDelay` and emits the `…Reanchored` event; it never stores a zero-derived seed. @@ -177,7 +178,7 @@ A lock call reverts if the seed isn't anchored yet (`TC_*SeedNotAnchored`), is a - **Post-lock pool reshaping.** The active pools are read when `createAuditorsBatches` / `autoCreateTier1AndTier2` run, after the seed is public. A validator can unstake between the lock and the create call, which changes the pool size and so reshuffles everyone. An attacker with several registered addresses can compute each outcome off-chain and unstake whichever address gives the batch they want. ### 6.4 `autoCreateTier1AndTier2` -Requires `LMSevaluationClosed` and a locked `aggSeed[gi]`. +Requires `AggSeedLocked`, i.e. a locked `aggSeed[gi]`. 1. Filters the registered aggregators to those still `Active` at call time (`_activeAggregatorPool`); needs ≥ 3 (`TC_NotEnoughValidators`). 2. Shuffles the aggregators with `keccak256(seed, "AGG_ADDR")` and the approved model indexes with `keccak256(seed, "AGG_IDX")` (Fisher-Yates, in memory). Needs ≥ `T1_MODELS_PER_BATCH` approved models (`TC_NotEnoughApprovedModels`). 3. Greedily fills T1 batches with 3 aggregators and 3 models each (the last batch may take 2 models). @@ -279,7 +280,7 @@ Earlier findings from the [foundry/src security review](../audits/foundry-src-se - **No. 5 — Plurality with three aggregators.** Two colluding aggregators in a batch win the vote; nothing verifies the aggregated model itself. The dispute path (§7) is the only recourse, and the owner adjudicates it. - **No. 6 — No recovery from a stalled GI.** If a batch never reaches the reveal quorum, `finalizeT1Aggregation` / `finalizeT2Aggregation` keep reverting and there is no owner path to skip the batch or abort the GI. - **No. 7 — Stale NatSpec:** comments around the dispute scaffold still say `DinTreasury` "doesn't exist on develop yet" (forfeitures are already forwarded). Several parameters are described as "DAO-settable"; they are `onlyOwner`, i.e. set by the model owner. -- **No. 8 — Leftovers:** `networkFeeFloor` is stored but not enforced. `setTestDataAssignedFlag` gates nothing: evaluation can start without test data being assigned. `releaseGIRegistrationSlots` uses string `require` messages, unlike the rest of the contract. +- **No. 8 — Leftovers:** `networkFeeFloor` is stored but not enforced. - **No. 9 — Not upgradeable:** a bug in a model's task contracts requires redeploying them and re-registering the model. - **No. 10 — dincli lags this contract:** `dincli model-owner deploy task-coordinator` still calls the older one-argument constructor (no `modelId`). (`dincli aggregator aggregate-t2` used to name its working directory, worker job and container after the last T1 batch id; fixed in issue No. 202 Part 2.) - **No. 11 — Fixed: `registerDINaggregator` now checks the GI.** It used to have no `onlyCurrentGI`, so during GI N's window a validator could register for GI N+1. Up to 300 addresses could fill that list, which locked out honest registrants and handed the attacker every T1/T2 batch. Registering for a released past GI also leaked the caller's own slot. Fixed by adding `onlyCurrentGI` (issue #206); it cost 9 bytes inside the No. 1 budget. @@ -299,6 +300,7 @@ Earlier findings from the [foundry/src security review](../audits/foundry-src-se - Forfeited dispute bonds forwarded to the platform treasury (`slashTreasury()`), replacing the per-contract treasury address (issue #152). - `GIStateChanged` emitted on every state change; `T1BatchFinalized` / `T2Finalized` on finalization. - `modelId` constructor argument. +- Progress states (task_061026_21 Part B): `lockAuditSeed` → `AuditSeedLocked`, `setTestDataAssignedFlag` → `AuditTestDataAssigned`, `lockAggSeed` → `AggSeedLocked`. `createAuditorsBatches`, `startLMsubmissionsEvaluation` and `autoCreateTier1AndTier2` are gated on them, so evaluation can no longer start before every batch has test data. `GIended` moved from 25 to 28; `TC_EvalPhaseNotClosed` is gone. To fit the coordinator's size budget, `releaseGIRegistrationSlots`' two string `require`s became `TC_GINotEnded` / `TC_SlotsAlreadyReleased` (−88 B). - Locked batch-assignment seeds for auditor and T1/T2 batches (`lockAuditSeed`, `lockAggSeed`; issue #156 H-2, PR #191). - Commit-then-reveal T1/T2 aggregation with a sender-bound commit hash (`commitT*Aggregation`, `revealT*Aggregation`, `startT*AggregationReveal`; issue #156 M-1, PR #197). Adds states `T1AggregationRevealStarted` (18) and `T2AggregationRevealStarted` (21), shifting later ordinals. - Back under EIP-170 (issue #201 Part A). The `slashAggregators` T1/T2 loops are folded into `_slashBatch`, with unchanged slashes and events. `tier1Batches` / `tier2Batches` and the ten `t1*`/`t2*` per-aggregator maps are now `internal`, and the new `getAggregatorSubmission` view replaces their getters. This is a view-ABI change; dincli and the bundled ABI were updated with it. diff --git a/dincli/abis/DINTaskAuditor.json b/dincli/abis/DINTaskAuditor.json index 2a6bb6c9..92ce5452 100644 --- a/dincli/abis/DINTaskAuditor.json +++ b/dincli/abis/DINTaskAuditor.json @@ -2483,11 +2483,6 @@ "name": "TA_EncryptedKeyCountMismatch", "inputs": [] }, - { - "type": "error", - "name": "TA_FlagAlreadySet", - "inputs": [] - }, { "type": "error", "name": "TA_FlagMustBeTrue", @@ -2623,6 +2618,11 @@ "name": "TA_StakeBelowModelFloor", "inputs": [] }, + { + "type": "error", + "name": "TA_TestDataNotAssigned", + "inputs": [] + }, { "type": "error", "name": "TA_WrongGI", diff --git a/dincli/abis/DINTaskCoordinator.json b/dincli/abis/DINTaskCoordinator.json index 20af9c1e..66c9ee7b 100644 --- a/dincli/abis/DINTaskCoordinator.json +++ b/dincli/abis/DINTaskCoordinator.json @@ -2412,11 +2412,6 @@ "name": "TC_DisputeWindowClosed", "inputs": [] }, - { - "type": "error", - "name": "TC_EvalPhaseNotClosed", - "inputs": [] - }, { "type": "error", "name": "TC_FailedToCreateAuditorsBatches", @@ -2437,6 +2432,11 @@ "name": "TC_GICannotBeStarted", "inputs": [] }, + { + "type": "error", + "name": "TC_GINotEnded", + "inputs": [] + }, { "type": "error", "name": "TC_GIRewardPoolNotFunded", @@ -2577,6 +2577,11 @@ "name": "TC_RevealCannotBeStarted", "inputs": [] }, + { + "type": "error", + "name": "TC_SlotsAlreadyReleased", + "inputs": [] + }, { "type": "error", "name": "TC_StakeBelowModelFloor", diff --git a/dincli/cli/modelownerd/aggregation.py b/dincli/cli/modelownerd/aggregation.py index 5ffc0c95..bd42c87c 100644 --- a/dincli/cli/modelownerd/aggregation.py +++ b/dincli/cli/modelownerd/aggregation.py @@ -2,7 +2,7 @@ import typer from rich.table import Table -from dincli.cli.utils import (build_and_send_tx, ensure_batch_seed_locked, +from dincli.cli.utils import (GIstateToStr, build_and_send_tx, ensure_batch_seed_locked, get_manifest_key, require_custom_manifest_service, resolve_manifest_path) from dincli.services.cid_utils import get_cid_from_bytes32 @@ -27,16 +27,18 @@ def create_tier1_tier2_batches( curr_GI, GIstate = ctx.obj.get_current_gi_and_state(task_coordinator_Contract) ref_gi = ctx.obj.validate_gi_ET_curr_GI(gi, curr_GI) - ctx.obj.validate_GIstate_ET_given_GIstate(GIstate, "LMSevaluationClosed","Can not create Tier 1 & Tier 2 batches at this time.") - # issue #156 H-2: autoCreateTier1AndTier2 now requires an ungrindable, # future-block-anchored seed to already be locked. Lock it here (or # skip if someone else already has) before attempting the real call. - ensure_batch_seed_locked( - ctx, task_coordinator_Contract, ref_gi, - seed_getter="aggSeed", seed_block_getter="aggSeedBlock", - lock_fn="lockAggSeed", label="T1/T2 batch", - ) + # A successful lock moves the GI to AggSeedLocked (task_061026_21 + # Part B), so only LMSevaluationClosed still needs it. + if GIstateToStr(GIstate) != "AggSeedLocked": + ctx.obj.validate_GIstate_ET_given_GIstate(GIstate, "LMSevaluationClosed","Can not create Tier 1 & Tier 2 batches at this time.") + ensure_batch_seed_locked( + ctx, task_coordinator_Contract, ref_gi, + seed_getter="aggSeed", seed_block_getter="aggSeedBlock", + lock_fn="lockAggSeed", label="T1/T2 batch", + ) console.print(f"[bold green]Creating Tier 1 & Tier 2 batches[/bold green]") diff --git a/dincli/cli/modelownerd/auditor_batches.py b/dincli/cli/modelownerd/auditor_batches.py index 7babe820..d2ddc6cf 100644 --- a/dincli/cli/modelownerd/auditor_batches.py +++ b/dincli/cli/modelownerd/auditor_batches.py @@ -10,7 +10,7 @@ from eth_account.messages import encode_defunct from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from dincli.cli.utils import CONFIG_DIR, build_and_send_tx, ensure_batch_seed_locked, get_manifest_key, require_custom_manifest_service, resolve_manifest_path +from dincli.cli.utils import CONFIG_DIR, GIstateToStr, build_and_send_tx, ensure_batch_seed_locked, get_manifest_key, require_custom_manifest_service, resolve_manifest_path from dincli.services.cid_utils import get_bytes32_from_cid, get_cid_from_bytes32 auditor_batches_app = typer.Typer(help="Auditor Batches commands") @@ -29,16 +29,18 @@ def create( ref_gi = ctx.obj.validate_gi_ET_curr_GI(gi, curr_GI) - ctx.obj.validate_GIstate_ET_given_GIstate(GIstate, "LMSclosed", "Can not create auditor batches at this time") - # issue #156 H-2: createAuditorsBatches now requires an ungrindable, # future-block-anchored seed to already be locked. Lock it here (or # skip if someone else already has) before attempting the real call. - ensure_batch_seed_locked( - ctx, taskCoordinator_contract, ref_gi, - seed_getter="auditSeed", seed_block_getter="auditSeedBlock", - lock_fn="lockAuditSeed", label="auditor-batch", - ) + # A successful lock moves the GI to AuditSeedLocked (task_061026_21 + # Part B), so only LMSclosed still needs it. + if GIstateToStr(GIstate) != "AuditSeedLocked": + ctx.obj.validate_GIstate_ET_given_GIstate(GIstate, "LMSclosed", "Can not create auditor batches at this time") + ensure_batch_seed_locked( + ctx, taskCoordinator_contract, ref_gi, + seed_getter="auditSeed", seed_block_getter="auditSeedBlock", + lock_fn="lockAuditSeed", label="auditor-batch", + ) console.print(f"[bold green]Creating auditor batches [/bold green]") diff --git a/dincli/cli/modelownerd/lms_evaluation.py b/dincli/cli/modelownerd/lms_evaluation.py index 7f285753..8c2c0a47 100644 --- a/dincli/cli/modelownerd/lms_evaluation.py +++ b/dincli/cli/modelownerd/lms_evaluation.py @@ -21,7 +21,7 @@ def start( curr_GI, GIstate = ctx.obj.get_current_gi_and_state(task_coordinator_Contract) ref_gi = ctx.obj.validate_gi_ET_curr_GI(gi, curr_GI) - ctx.obj.validate_GIstate_ET_given_GIstate(GIstate, "AuditorsBatchesCreated", "Can not start LMS evaluation at this time") + ctx.obj.validate_GIstate_ET_given_GIstate(GIstate, "AuditTestDataAssigned", "Can not start LMS evaluation at this time (run `auditor-batches create-testdataset --submit` first)") console.print(f"[bold green]Starting LMS evaluation[/bold green]") diff --git a/dincli/cli/utils.py b/dincli/cli/utils.py index 8f9b51ea..717ea0b2 100644 --- a/dincli/cli/utils.py +++ b/dincli/cli/utils.py @@ -681,10 +681,13 @@ def save_din_info(data: dict): "DIN auditors registration closed", "LM submissions started", "LM submissions closed", + "Audit seed locked", "Auditors batches created", + "Audit test data assigned", "LM submissions evaluation started", "LM submissions evaluation reveal started", "LM submissions evaluation closed", + "Aggregation seed locked", "T1nT2B created", "T1B aggregation started", "T1B aggregation reveal started", @@ -693,7 +696,7 @@ def save_din_info(data: dict): "T2B aggregation reveal started", "T2B aggregation done", "Auditors slashed", - "Validators slashed", + "Aggregators slashed", "GI ended", ] @@ -710,10 +713,15 @@ def save_din_info(data: dict): "DINauditorsRegistrationClosed", "LMSstarted", "LMSclosed", + # task_061026_21 Part B: progress states, inserted in lifecycle + # position (same precedent as the reveal states below). + "AuditSeedLocked", "AuditorsBatchesCreated", + "AuditTestDataAssigned", "LMSevaluationStarted", "LMSevaluationRevealStarted", "LMSevaluationClosed", + "AggSeedLocked", "T1nT2Bcreated", "T1AggregationStarted", # issue #156 M-1 (task_240926_18 Part C): inserted in lifecycle diff --git a/foundry/src/DINShared.sol b/foundry/src/DINShared.sol index a27f1e81..0d3b3610 100644 --- a/foundry/src/DINShared.sol +++ b/foundry/src/DINShared.sol @@ -20,8 +20,17 @@ enum GIstates { DINauditorsRegistrationClosed, // 9 LMSstarted, // 10 LMSclosed, // 11 - AuditorsBatchesCreated, // 12 - LMSevaluationStarted, // 13 + // Progress states (task_061026_21 Part B). Each is set by the function + // that already does the step -- lockAuditSeed, setTestDataAssignedFlag, + // lockAggSeed -- so a GI needs no extra owner transaction. Inserted at + // their lifecycle positions (same 2026-08-27 precedent as the reveal + // states below), shifting every later ordinal; dincli/cli/utils.py's + // `states`/`stateDescription` mirrors and + // Documentation/technical/contracts/DINShared.md §2.1 match. + AuditSeedLocked, // 12 + AuditorsBatchesCreated, // 13 + AuditTestDataAssigned, // 14 + LMSevaluationStarted, // 15 // Reveal phase for commit-then-reveal auditor scoring (task_210726_6 §2a). // Inserted here, immediately after the commit phase, rather than // appended at the end as PR #63's own diff originally did (see git @@ -31,11 +40,12 @@ enum GIstates { // (indexed by this enum's raw ordinals) have been updated to match, // shifting every subsequent entry by +1; see that file and // Documentation/technical/contracts/DINShared.md §2.1 for the full - // 24-member table. - LMSevaluationRevealStarted, // 14 - LMSevaluationClosed, // 15 - T1nT2Bcreated, // 16 - T1AggregationStarted, // 17 + // 29-member table. + LMSevaluationRevealStarted, // 16 + LMSevaluationClosed, // 17 + AggSeedLocked, // 18 + T1nT2Bcreated, // 19 + T1AggregationStarted, // 20 // Reveal phase for commit-then-reveal T1/T2 aggregation submissions // (issue #156 M-1, task_240926_18 Part C). Same 2026-08-27 precedent as // LMSevaluationRevealStarted above: inserted immediately after each @@ -43,14 +53,14 @@ enum GIstates { // `stateDescription` mirrors and Documentation/technical/contracts/ // DINShared.md §2.1 have been updated to match, shifting every // subsequent entry by +1 (T1) then +1 again (T2). - T1AggregationRevealStarted, // 18 - T1AggregationDone, // 19 - T2AggregationStarted, // 20 - T2AggregationRevealStarted, // 21 - T2AggregationDone, // 22 - AuditorsSlashed, // 23 - AggregatorsSlashed, // 24 - GIended // 25 + T1AggregationRevealStarted, // 21 + T1AggregationDone, // 22 + T2AggregationStarted, // 23 + T2AggregationRevealStarted, // 24 + T2AggregationDone, // 25 + AuditorsSlashed, // 26 + AggregatorsSlashed, // 27 + GIended // 28 } // ───────────────────────────────────────────────────────────────────────────── @@ -213,8 +223,8 @@ error TA_BatchIDMismatch(); error TA_CannotSetTestDataAssignedFlag(); /// @dev The flag value supplied must be true; false is not accepted here. error TA_FlagMustBeTrue(); -/// @dev The test data assigned flag for this batch has already been set. -error TA_FlagAlreadySet(); +/// @dev setTestDataAssignedFlag: some audit batch of the GI has no stored test-data commitment yet. +error TA_TestDataNotAssigned(); /// @dev Caller is not assigned to the target auditor batch. error TA_NotAssignedAuditor(); /// @dev The supplied model index is outside the range of submitted models. @@ -301,8 +311,10 @@ error TC_LMEvalCannotBeFinished(); error TC_FailedToCreateAuditorsBatches(); /// @dev GI state does not permit setting the test data assigned flag. error TC_CannotSetTestDataAssignedFlag(); -/// @dev LMS evaluation phase has not been closed yet. -error TC_EvalPhaseNotClosed(); +/// @dev releaseGIRegistrationSlots: the GI has not ended yet. +error TC_GINotEnded(); +/// @dev releaseGIRegistrationSlots: this GI's slots were already released. +error TC_SlotsAlreadyReleased(); /// @dev Fewer active validators registered than required to form T1 batches. error TC_NotEnoughValidators(); /// @dev Fewer models passed evaluation than the minimum required for T2 aggregation. diff --git a/foundry/src/DINTaskAuditor.sol b/foundry/src/DINTaskAuditor.sol index 342222a0..ea11dfb7 100644 --- a/foundry/src/DINTaskAuditor.sol +++ b/foundry/src/DINTaskAuditor.sol @@ -259,8 +259,6 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { mapping(uint256 => mapping(uint => mapping(address => mapping(uint => bool)))) // GI // batchId // auditor // modelIndex // has committed public hasCommittedLM; - mapping(uint256 => bool) internal Is_testdataCIDs_Assigned; - // §2b: per-validator encrypted test-data key mapping. GI => batchId => // auditor => that auditor's copy of the test-data decryption key, // encrypted to their public key off-chain. Populated in @@ -872,7 +870,7 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { uint _GI, bytes32 seed ) external onlyTaskCoordinator onlyCurrentGI(_GI) returns (bool) { - if (dintaskcoordinatorContract.GIstate() != GIstates.LMSclosed) + if (dintaskcoordinatorContract.GIstate() != GIstates.AuditSeedLocked) revert TA_CannotCreateAuditorsBatches(); if (seed == bytes32(0)) revert TA_AuditSeedNotLocked(); @@ -1025,8 +1023,12 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { } /// @notice Marks test dataset distribution as complete for the given GI. - /// @dev Restricted to the paired DINTaskCoordinator. flag must be true; - /// can only be set once per GI. + /// @dev Restricted to the paired DINTaskCoordinator. flag must be true, + /// and every audit batch of the GI must have a stored test-data + /// commitment (written by assignAuditTestDataset), else + /// TA_TestDataNotAssigned. One-way: the coordinator then moves to + /// AuditTestDataAssigned, so the AuditorsBatchesCreated gate below + /// rejects a second call (task_061026_21 Part B). /// @param _GI Current GI index. /// @param flag Must be true. /// @return True on success. @@ -1039,9 +1041,11 @@ contract DINTaskAuditor is Ownable, ReentrancyGuardTransient { GIstates.AuditorsBatchesCreated ) revert TA_CannotSetTestDataAssignedFlag(); if (!flag) revert TA_FlagMustBeTrue(); - if (Is_testdataCIDs_Assigned[_GI]) revert TA_FlagAlreadySet(); - - Is_testdataCIDs_Assigned[_GI] = flag; + uint256 n = auditBatches[_GI].length; + for (uint256 b = 0; b < n; b++) { + if (testDataCommitments[_GI][b] == bytes32(0)) + revert TA_TestDataNotAssigned(); + } return true; } diff --git a/foundry/src/DINTaskCoordinator.sol b/foundry/src/DINTaskCoordinator.sol index 709cd542..56aca444 100644 --- a/foundry/src/DINTaskCoordinator.sol +++ b/foundry/src/DINTaskCoordinator.sol @@ -494,26 +494,29 @@ contract DINTaskCoordinator is Ownable, ReentrancyGuardTransient { /// @notice Delegates auditor batch creation to DINTaskAuditor and advances GI state. /// @dev Reverts if the auditor contract returns false (e.g. insufficient auditors). - /// Requires auditSeed[_GI] to already be locked via lockAuditSeed - /// (issue #156 H-2) -- the locked seed is passed across the interface - /// so DINTaskAuditor's shuffles are ungrindable without duplicating - /// the seed-anchoring logic in that contract. + /// Requires the AuditSeedLocked state, i.e. auditSeed[_GI] already + /// locked via lockAuditSeed (issue #156 H-2) -- the locked seed is + /// passed across the interface so DINTaskAuditor's shuffles are + /// ungrindable without duplicating the seed-anchoring logic in that + /// contract. /// @param _GI Current GI index. function createAuditorsBatches( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { - if (GIstate != GIstates.LMSclosed) revert TC_LMEvalCannotBeStarted(); - bytes32 seed = auditSeed[_GI]; - if (seed == bytes32(0)) revert TC_AuditSeedNotLocked(); + if (GIstate != GIstates.AuditSeedLocked) revert TC_AuditSeedNotLocked(); - bool success = dinTaskAuditorContract.createAuditorsBatches(_GI, seed); + bool success = dinTaskAuditorContract.createAuditorsBatches(_GI, auditSeed[_GI]); if (!success) revert TC_FailedToCreateAuditorsBatches(); _setGIstate(GIstates.AuditorsBatchesCreated); } - /// @notice Propagates the test data assignment flag to DINTaskAuditor. - /// @dev Must be called while GI state is AuditorsBatchesCreated. + /// @notice Propagates the test data assignment flag to DINTaskAuditor and + /// advances to AuditTestDataAssigned. + /// @dev Must be called while GI state is AuditorsBatchesCreated. The + /// auditor side reverts TA_TestDataNotAssigned unless every batch of + /// the GI has a stored test-data commitment. Leaving + /// AuditorsBatchesCreated is what makes the flag one-way. /// @param _GI Current GI index. /// @param flag True once test datasets have been distributed to auditors. function setTestDataAssignedFlag( @@ -524,15 +527,18 @@ contract DINTaskCoordinator is Ownable, ReentrancyGuardTransient { revert TC_CannotSetTestDataAssignedFlag(); dinTaskAuditorContract.setTestDataAssignedFlag(_GI, flag); + _setGIstate(GIstates.AuditTestDataAssigned); } /// @notice Opens the LMS evaluation COMMIT phase so auditors can begin /// committing (hidden) scores via DINTaskAuditor.commitAuditScore. + /// @dev Requires AuditTestDataAssigned, so evaluation can't start before + /// every batch has test data (task_061026_21 Part B). /// @param _GI Current GI index. function startLMsubmissionsEvaluation( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { - if (GIstate != GIstates.AuditorsBatchesCreated) + if (GIstate != GIstates.AuditTestDataAssigned) revert TC_LMEvalCannotBeStarted(); _setGIstate(GIstates.LMSevaluationStarted); } @@ -577,15 +583,13 @@ contract DINTaskCoordinator is Ownable, ReentrancyGuardTransient { /// choosing when to call this. Reverts if fewer than /// T1_AGGREGATORS_PER_BATCH active validators remain, or if fewer than /// T1_MODELS_PER_BATCH models passed, or if the seed isn't locked yet - /// (call lockAggSeed first). + /// (call lockAggSeed first, which moves the GI to AggSeedLocked). /// @param _GI Current GI index. function autoCreateTier1AndTier2( uint _GI ) external onlyOwner onlyCurrentGI(_GI) { - if (GIstate != GIstates.LMSevaluationClosed) - revert TC_EvalPhaseNotClosed(); + if (GIstate != GIstates.AggSeedLocked) revert TC_AggSeedNotLocked(); bytes32 seed = aggSeed[_GI]; - if (seed == bytes32(0)) revert TC_AggSeedNotLocked(); // Filter the historical registration list down to currently active validators. address[] memory valPool = _activeAggregatorPool(_GI); @@ -1287,8 +1291,8 @@ contract DINTaskCoordinator is Ownable, ReentrancyGuardTransient { function releaseGIRegistrationSlots(uint _GI) external onlyOwner { bool giEnded = (_GI < GI) || (_GI == GI && GIstate == GIstates.GIended); - require(giEnded, "GI has not ended"); - require(!registrationSlotsReleased[_GI], "slots already released"); + if (!giEnded) revert TC_GINotEnded(); + if (registrationSlotsReleased[_GI]) revert TC_SlotsAlreadyReleased(); registrationSlotsReleased[_GI] = true; address[] storage aggs = dinAggregators[_GI]; @@ -1477,6 +1481,9 @@ contract DINTaskCoordinator is Ownable, ReentrancyGuardTransient { aggSeed[_GI] = keccak256(abi.encodePacked(bh, _GI, "AGG")); emit AggSeedLocked(_GI, aggSeed[_GI]); + // aggSeedBlock is only anchored by closeLMsubmissionsEvaluation and + // the seed locks once, so the GI is in LMSevaluationClosed here. + _setGIstate(GIstates.AggSeedLocked); } /// @notice Permissionless: locks the entropy seed for the current GI's @@ -1501,6 +1508,9 @@ contract DINTaskCoordinator is Ownable, ReentrancyGuardTransient { auditSeed[_GI] = keccak256(abi.encodePacked(bh, _GI, "AUD")); emit AuditSeedLocked(_GI, auditSeed[_GI]); + // auditSeedBlock is only anchored by closeLMsubmissions and the seed + // locks once, so the GI is in LMSclosed here. + _setGIstate(GIstates.AuditSeedLocked); } /// @dev Burns 50% of `amount` and forwards 50% to the platform slash-treasury diff --git a/foundry/test/AggregatorCommitReveal.t.sol b/foundry/test/AggregatorCommitReveal.t.sol index dff98776..892954de 100644 --- a/foundry/test/AggregatorCommitReveal.t.sol +++ b/foundry/test/AggregatorCommitReveal.t.sol @@ -38,6 +38,7 @@ import { TC_T2RevealPhaseNotOpen } from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract AggregatorCommitRevealTest is Test { DinToken tokenImpl; @@ -215,6 +216,7 @@ contract AggregatorCommitRevealTest is Test { tc.closeLMsubmissions(1); _lockAuditSeedNow(1); // issue #156 H-2 tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/AuditorCommitReveal.t.sol b/foundry/test/AuditorCommitReveal.t.sol index d1963ce1..b682ae30 100644 --- a/foundry/test/AuditorCommitReveal.t.sol +++ b/foundry/test/AuditorCommitReveal.t.sol @@ -19,6 +19,7 @@ import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates, TA_RevealHashMismatch} from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract AuditorCommitRevealTest is Test { DinToken tokenImpl; @@ -189,6 +190,7 @@ contract AuditorCommitRevealTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/DisputeResolution.t.sol b/foundry/test/DisputeResolution.t.sol index 97efb264..904eaf7d 100644 --- a/foundry/test/DisputeResolution.t.sol +++ b/foundry/test/DisputeResolution.t.sol @@ -21,6 +21,7 @@ import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import "../src/DINShared.sol" as Shared; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; // Pull the custom errors into scope so vm.expectRevert(Error.selector) compiles. error TC_DisputeNotAwaitingRecomputation(); @@ -238,6 +239,7 @@ contract DisputeResolutionTest is Test { tc.closeLMsubmissions(1); _lockAuditSeedNow(1); // issue #156 H-2 tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/GasSimulation.t.sol b/foundry/test/GasSimulation.t.sol index 32b1c8c1..d7d9dc26 100644 --- a/foundry/test/GasSimulation.t.sol +++ b/foundry/test/GasSimulation.t.sol @@ -12,6 +12,7 @@ import {DinTreasury} from "../src/DinTreasury.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; /// @notice Gas simulation for issue #78 — validator network-fee sizing. /// @@ -154,6 +155,7 @@ contract GasSimulationTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(gi); tc.createAuditorsBatches(gi); + assignPlaceholderTestData(ta, address(stake), gi); tc.setTestDataAssignedFlag(gi, true); tc.startLMsubmissionsEvaluation(gi); } diff --git a/foundry/test/LifecycleEvents.t.sol b/foundry/test/LifecycleEvents.t.sol index 75f443f7..1d72cc5c 100644 --- a/foundry/test/LifecycleEvents.t.sol +++ b/foundry/test/LifecycleEvents.t.sol @@ -11,6 +11,8 @@ pragma solidity ^0.8.28; // - T2AggregationSubmitted emitted by revealT2Aggregation (same split) // - T1BatchFinalized emitted per batch in finalizeT1Aggregation // - T2Finalized emitted in finalizeT2Aggregation +// - task_061026_21 Part B progress states: AuditSeedLocked, +// AuditTestDataAssigned, AggSeedLocked, and the gates that need them // Run: forge test --match-contract LifecycleEventsTest -vv // ───────────────────────────────────────────────────────────────────────────── @@ -22,8 +24,16 @@ import {DinCoordinator} from "../src/DinCoordinator.sol"; 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 { + GIstates, + TA_TestDataNotAssigned, + TC_AggSeedNotLocked, + TC_AuditSeedNotLocked, + TC_CannotSetTestDataAssignedFlag, + TC_LMEvalCannotBeStarted +} from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract LifecycleEventsTest is Test { DinToken token; @@ -66,20 +76,23 @@ contract LifecycleEventsTest is Test { uint8 constant GI_AUD_REG_CLOSED = 9; uint8 constant GI_LMS_STARTED = 10; uint8 constant GI_LMS_CLOSED = 11; - uint8 constant GI_AUDITOR_BATCHES_CREATED = 12; - uint8 constant GI_LMS_EVAL_STARTED = 13; - uint8 constant GI_LMS_EVAL_REVEAL_STARTED = 14; - uint8 constant GI_LMS_EVAL_CLOSED = 15; - uint8 constant GI_T1T2_CREATED = 16; - uint8 constant GI_T1_AGG_STARTED = 17; - uint8 constant GI_T1_AGG_REVEAL_STARTED = 18; - uint8 constant GI_T1_AGG_DONE = 19; - uint8 constant GI_T2_AGG_STARTED = 20; - uint8 constant GI_T2_AGG_REVEAL_STARTED = 21; - uint8 constant GI_T2_AGG_DONE = 22; - uint8 constant GI_AUDITORS_SLASHED = 23; - uint8 constant GI_AGGREGATORS_SLASHED = 24; - uint8 constant GI_ENDED = 25; + uint8 constant GI_AUDIT_SEED_LOCKED = 12; + uint8 constant GI_AUDITOR_BATCHES_CREATED = 13; + uint8 constant GI_TEST_DATA_ASSIGNED = 14; + uint8 constant GI_LMS_EVAL_STARTED = 15; + uint8 constant GI_LMS_EVAL_REVEAL_STARTED = 16; + uint8 constant GI_LMS_EVAL_CLOSED = 17; + uint8 constant GI_AGG_SEED_LOCKED = 18; + uint8 constant GI_T1T2_CREATED = 19; + uint8 constant GI_T1_AGG_STARTED = 20; + uint8 constant GI_T1_AGG_REVEAL_STARTED = 21; + uint8 constant GI_T1_AGG_DONE = 22; + uint8 constant GI_T2_AGG_STARTED = 23; + uint8 constant GI_T2_AGG_REVEAL_STARTED = 24; + uint8 constant GI_T2_AGG_DONE = 25; + uint8 constant GI_AUDITORS_SLASHED = 26; + uint8 constant GI_AGGREGATORS_SLASHED = 27; + uint8 constant GI_ENDED = 28; function setUp() public { vm.startPrank(admin); @@ -208,6 +221,19 @@ contract LifecycleEventsTest is Test { /// T1AggregationStarted, committing/revealing in every auditor batch. /// 6 approved models + 6 aggregators yield two Tier-1 batches. function _advanceToT1(uint256 giIndex, uint256 nClients) internal { + _advanceToEvalClosed(giIndex, nClients); + vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock + tc.lockAggSeed(giIndex); + + vm.startPrank(modelOwner); + tc.autoCreateTier1AndTier2(giIndex); + tc.startT1Aggregation(giIndex); + vm.stopPrank(); + } + + /// @dev Submits `nClients` (3 or 6) local models and closes LMS, which + /// anchors the audit seed. + function _submitAndCloseLMS(uint256 giIndex, uint256 nClients) internal { address[6] memory clients = [client1, client2, client3, client4, client5, client6]; for (uint i = 0; i < nClients; i++) { vm.prank(clients[i]); ta.submitLocalModel(bytes32((i + 1) * 100), giIndex); @@ -216,11 +242,18 @@ contract LifecycleEventsTest is Test { vm.startPrank(modelOwner); tc.closeLMsubmissions(giIndex); vm.stopPrank(); + } + + /// @dev Same as _advanceToT1 up to LMSevaluationClosed (aggregation seed + /// anchored, not locked). + function _advanceToEvalClosed(uint256 giIndex, uint256 nClients) internal { + _submitAndCloseLMS(giIndex, nClients); vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(giIndex); vm.startPrank(modelOwner); tc.createAuditorsBatches(giIndex); + assignPlaceholderTestData(ta, address(stake), giIndex); tc.setTestDataAssignedFlag(giIndex, true); tc.startLMsubmissionsEvaluation(giIndex); vm.stopPrank(); @@ -252,13 +285,6 @@ contract LifecycleEventsTest is Test { vm.startPrank(modelOwner); tc.closeLMsubmissionsEvaluation(giIndex); vm.stopPrank(); - vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock - tc.lockAggSeed(giIndex); - - vm.startPrank(modelOwner); - tc.autoCreateTier1AndTier2(giIndex); - tc.startT1Aggregation(giIndex); - vm.stopPrank(); } /// @dev issue #156 M-1: commits `who` to `cid` for T1/T2 batch `batchId`. @@ -559,9 +585,11 @@ contract LifecycleEventsTest is Test { emit DINTaskCoordinator.GIStateChanged(1, GI_LMS_CLOSED); vm.prank(modelOwner); tc.closeLMsubmissions(1); - // issue #156 H-2: seed lock (emits its own event, so this must land - // before the expectEmit below rather than between it and the call) + // AuditSeedLocked (task_061026_21 Part B): the permissionless seed + // lock (issue #156 H-2) now also advances the state vm.roll(block.number + tc.disputeSeedDelay() + 1); + vm.expectEmit(true, true, false, false, address(tc)); + emit DINTaskCoordinator.GIStateChanged(1, GI_AUDIT_SEED_LOCKED); tc.lockAuditSeed(1); // AuditorsBatchesCreated @@ -569,7 +597,13 @@ contract LifecycleEventsTest is Test { emit DINTaskCoordinator.GIStateChanged(1, GI_AUDITOR_BATCHES_CREATED); vm.prank(modelOwner); tc.createAuditorsBatches(1); - vm.prank(modelOwner); tc.setTestDataAssignedFlag(1, true); + // AuditTestDataAssigned (task_061026_21 Part B) + vm.startPrank(modelOwner); + assignPlaceholderTestData(ta, address(stake), 1); + vm.expectEmit(true, true, false, false, address(tc)); + emit DINTaskCoordinator.GIStateChanged(1, GI_TEST_DATA_ASSIGNED); + tc.setTestDataAssignedFlag(1, true); + vm.stopPrank(); // LMSevaluationStarted vm.expectEmit(true, true, false, false, address(tc)); @@ -603,9 +637,10 @@ contract LifecycleEventsTest is Test { emit DINTaskCoordinator.GIStateChanged(1, GI_LMS_EVAL_CLOSED); vm.prank(modelOwner); tc.closeLMsubmissionsEvaluation(1); - // issue #156 H-2: seed lock (emits its own event, so this must land - // before the expectEmit below rather than between it and the call) + // AggSeedLocked (task_061026_21 Part B) vm.roll(block.number + tc.disputeSeedDelay() + 1); + vm.expectEmit(true, true, false, false, address(tc)); + emit DINTaskCoordinator.GIStateChanged(1, GI_AGG_SEED_LOCKED); tc.lockAggSeed(1); // T1nT2Bcreated @@ -684,4 +719,122 @@ contract LifecycleEventsTest is Test { emit DINTaskCoordinator.GIStateChanged(1, GI_ENDED); vm.prank(modelOwner); tc.endGI(1); } + + // ── task_061026_21 Part B: progress states ─────────────────────────────── + + function test_progressStates_createAuditorsBatchesRevertsBeforeAuditSeedLock() public { + _setupToLMS(1, 1_000 ether); + _submitAndCloseLMS(1, 3); + + vm.expectRevert(TC_AuditSeedNotLocked.selector); + vm.prank(modelOwner); + tc.createAuditorsBatches(1); + assertEq(uint8(tc.GIstate()), GI_LMS_CLOSED); + } + + function test_progressStates_auditSeedReanchorLeavesStateUnchanged() public { + _setupToLMS(1, 1_000 ether); + _submitAndCloseLMS(1, 3); + + vm.roll(uint256(tc.auditSeedBlock(1)) + 300); // > 256 blocks: re-anchors + tc.lockAuditSeed(1); + assertEq(tc.auditSeed(1), bytes32(0)); + assertEq(uint8(tc.GIstate()), GI_LMS_CLOSED); + + vm.roll(uint256(tc.auditSeedBlock(1)) + 1); + tc.lockAuditSeed(1); + assertTrue(tc.auditSeed(1) != bytes32(0)); + assertEq(uint8(tc.GIstate()), GI_AUDIT_SEED_LOCKED); + } + + /// @dev Regression for the gap task_061026_21 closes: evaluation used to + /// open straight from AuditorsBatchesCreated, with no test data. + function test_progressStates_evaluationCannotStartBeforeTestDataAssigned() public { + _setupToLMS(1, 1_000 ether); + _submitAndCloseLMS(1, 3); + vm.roll(block.number + tc.disputeSeedDelay() + 1); + tc.lockAuditSeed(1); + + vm.startPrank(modelOwner); + tc.createAuditorsBatches(1); + vm.expectRevert(TC_LMEvalCannotBeStarted.selector); + tc.startLMsubmissionsEvaluation(1); + vm.stopPrank(); + assertEq(uint8(tc.GIstate()), GI_AUDITOR_BATCHES_CREATED); + } + + function test_progressStates_oneUnassignedBatchBlocksFlagAndEvaluation() public { + _setupToLMS(1, 1_000 ether, 6); + _submitAndCloseLMS(1, 6); + vm.roll(block.number + tc.disputeSeedDelay() + 1); + tc.lockAuditSeed(1); + vm.prank(modelOwner); + tc.createAuditorsBatches(1); + assertEq(ta.AuditorsBatchCount(1), 2); + + vm.mockCall( + address(stake), abi.encodeWithSelector(stake.getEncryptionKey.selector), abi.encode(hex"01") + ); + (, address[] memory auds0, , ) = ta.getAuditorsBatch(1, 0); + (, address[] memory auds1, , ) = ta.getAuditorsBatch(1, 1); + + vm.startPrank(modelOwner); + ta.assignAuditTestDataset(1, 0, hex"01", new bytes[](auds0.length), keccak256("batch0")); + + vm.expectRevert(TA_TestDataNotAssigned.selector); + tc.setTestDataAssignedFlag(1, true); + vm.expectRevert(TC_LMEvalCannotBeStarted.selector); + tc.startLMsubmissionsEvaluation(1); + assertEq(uint8(tc.GIstate()), GI_AUDITOR_BATCHES_CREATED); + + ta.assignAuditTestDataset(1, 1, hex"01", new bytes[](auds1.length), keccak256("batch1")); + tc.setTestDataAssignedFlag(1, true); + assertEq(uint8(tc.GIstate()), GI_TEST_DATA_ASSIGNED); + + // One-way: leaving AuditorsBatchesCreated rejects a second call. + vm.expectRevert(TC_CannotSetTestDataAssignedFlag.selector); + tc.setTestDataAssignedFlag(1, true); + + tc.startLMsubmissionsEvaluation(1); + vm.stopPrank(); + assertEq(uint8(tc.GIstate()), GI_LMS_EVAL_STARTED); + } + + function test_progressStates_autoCreateRevertsBeforeAggSeedLock() public { + _setupToLMS(1, 1_000 ether); + _advanceToEvalClosed(1, 3); + + vm.expectRevert(TC_AggSeedNotLocked.selector); + vm.prank(modelOwner); + tc.autoCreateTier1AndTier2(1); + assertEq(uint8(tc.GIstate()), GI_LMS_EVAL_CLOSED); + } + + function test_progressStates_aggSeedReanchorLeavesStateUnchanged() public { + _setupToLMS(1, 1_000 ether); + _advanceToEvalClosed(1, 3); + + vm.roll(uint256(tc.aggSeedBlock(1)) + 300); + tc.lockAggSeed(1); + assertEq(tc.aggSeed(1), bytes32(0)); + assertEq(uint8(tc.GIstate()), GI_LMS_EVAL_CLOSED); + + vm.roll(uint256(tc.aggSeedBlock(1)) + 1); + tc.lockAggSeed(1); + assertTrue(tc.aggSeed(1) != bytes32(0)); + assertEq(uint8(tc.GIstate()), GI_AGG_SEED_LOCKED); + + vm.prank(modelOwner); + tc.autoCreateTier1AndTier2(1); + assertEq(uint8(tc.GIstate()), GI_T1T2_CREATED); + } + + /// @dev The dincli `states` mirror and DINShared.md §2.1 are indexed by + /// these raw ordinals; pin the three inserts and the shifted end. + function test_progressStates_ordinals() public pure { + assertEq(uint8(GIstates.AuditSeedLocked), GI_AUDIT_SEED_LOCKED); + assertEq(uint8(GIstates.AuditTestDataAssigned), GI_TEST_DATA_ASSIGNED); + assertEq(uint8(GIstates.AggSeedLocked), GI_AGG_SEED_LOCKED); + assertEq(uint8(GIstates.GIended), GI_ENDED); + } } diff --git a/foundry/test/PR146SlashingRegression.t.sol b/foundry/test/PR146SlashingRegression.t.sol index dfb2c587..bfcafe17 100644 --- a/foundry/test/PR146SlashingRegression.t.sol +++ b/foundry/test/PR146SlashingRegression.t.sol @@ -22,6 +22,7 @@ import {DinFeeRouter} from "../src/DinFeeRouter.sol"; import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract PR146SlashingRegressionTest is Test { // ───────────────────────────────────────────────────────────────────── @@ -189,6 +190,7 @@ contract PR146SlashingRegressionTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/RewardEngine.t.sol b/foundry/test/RewardEngine.t.sol index 572b98f9..6efc2f5d 100644 --- a/foundry/test/RewardEngine.t.sol +++ b/foundry/test/RewardEngine.t.sol @@ -20,6 +20,7 @@ import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract RewardEngineTest is Test { using stdStorage for StdStorage; @@ -212,6 +213,7 @@ contract RewardEngineTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -951,6 +953,7 @@ contract RewardEngineTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/ScoringValidation.t.sol b/foundry/test/ScoringValidation.t.sol index 1b4444d9..4d089011 100644 --- a/foundry/test/ScoringValidation.t.sol +++ b/foundry/test/ScoringValidation.t.sol @@ -19,6 +19,7 @@ import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract ScoringValidationTest is Test { DinToken tokenImpl; @@ -185,6 +186,7 @@ contract ScoringValidationTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/SecurityFindings.t.sol b/foundry/test/SecurityFindings.t.sol index a36626f1..655c7f43 100644 --- a/foundry/test/SecurityFindings.t.sol +++ b/foundry/test/SecurityFindings.t.sol @@ -35,6 +35,7 @@ import { TA_AuditSeedNotLocked } from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract SecurityFindingsTest is Test { // ───────────────────────────────────────────────────────────────────── @@ -297,6 +298,7 @@ contract SecurityFindingsTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -434,6 +436,7 @@ contract SecurityFindingsTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -906,6 +909,7 @@ contract SecurityFindingsTest is Test { vm.prank(modelOwner); tc.createAuditorsBatches(1); vm.startPrank(modelOwner); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -960,6 +964,7 @@ contract SecurityFindingsTest is Test { vm.prank(modelOwner); tc.createAuditorsBatches(1); vm.startPrank(modelOwner); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -1005,6 +1010,7 @@ contract SecurityFindingsTest is Test { vm.prank(modelOwner); tc.createAuditorsBatches(1); vm.startPrank(modelOwner); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -1042,6 +1048,7 @@ contract SecurityFindingsTest is Test { vm.prank(modelOwner); tc.createAuditorsBatches(1); vm.startPrank(modelOwner); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/StakingEnforcement.t.sol b/foundry/test/StakingEnforcement.t.sol index 38cf069d..f0d1cdbc 100644 --- a/foundry/test/StakingEnforcement.t.sol +++ b/foundry/test/StakingEnforcement.t.sol @@ -22,11 +22,13 @@ import { TC_StakeBelowModelFloor, TC_ConcurrentRegistrationCapReached, TC_WrongGI, + TC_SlotsAlreadyReleased, TA_StakeBelowModelFloor, TA_ConcurrentRegistrationCapReached, TA_DualRoleNotAllowed } from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract StakingEnforcementTest is Test { DinToken token; @@ -394,6 +396,7 @@ contract StakingEnforcementTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); @@ -482,7 +485,7 @@ contract StakingEnforcementTest is Test { vm.startPrank(modelOwner); tc.releaseGIRegistrationSlots(1); - vm.expectRevert(bytes("slots already released")); + vm.expectRevert(TC_SlotsAlreadyReleased.selector); tc.releaseGIRegistrationSlots(1); vm.stopPrank(); } diff --git a/foundry/test/TreasuryForwarding.t.sol b/foundry/test/TreasuryForwarding.t.sol index 1674341e..783175d0 100644 --- a/foundry/test/TreasuryForwarding.t.sol +++ b/foundry/test/TreasuryForwarding.t.sol @@ -22,6 +22,7 @@ import {DINTaskCoordinator} from "../src/DINTaskCoordinator.sol"; import {DINTaskAuditor} from "../src/DINTaskAuditor.sol"; import {GIstates} from "../src/DINShared.sol"; import {auditCommitHash} from "./utils/AuditCommitHash.sol"; +import {assignPlaceholderTestData} from "./utils/PlaceholderTestData.sol"; contract TreasuryForwardingTest is Test { DinToken token; @@ -173,6 +174,7 @@ contract TreasuryForwardingTest is Test { vm.roll(block.number + tc.disputeSeedDelay() + 1); // issue #156 H-2: seed lock tc.lockAuditSeed(1); tc.createAuditorsBatches(1); + assignPlaceholderTestData(ta, address(stake), 1); tc.setTestDataAssignedFlag(1, true); tc.startLMsubmissionsEvaluation(1); vm.stopPrank(); diff --git a/foundry/test/utils/PlaceholderTestData.sol b/foundry/test/utils/PlaceholderTestData.sol new file mode 100644 index 00000000..e4d43123 --- /dev/null +++ b/foundry/test/utils/PlaceholderTestData.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Vm} from "forge-std/Vm.sol"; +import {DINTaskAuditor} from "../../src/DINTaskAuditor.sol"; +import {IDinValidatorStake} from "../../src/DINShared.sol"; + +Vm constant VM = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + +/// @dev Assigns placeholder test data to every audit batch of `gi`, so +/// setTestDataAssignedFlag's every-batch commitment check +/// (task_061026_21 Part B) passes in tests that don't exercise the +/// encrypted test-data path (EncryptedTestData.t.sol does). Auditor +/// X25519 keys are mocked on `stake` for the duration of the call, not +/// registered. Every call is made as the current caller, so run it as +/// the model owner (under vm.startPrank, or as the owning test contract). +function assignPlaceholderTestData(DINTaskAuditor ta, address stake, uint256 gi) { + VM.mockCall( + stake, + abi.encodeWithSelector(IDinValidatorStake.getEncryptionKey.selector), + abi.encode(hex"01") + ); + uint256 n = ta.AuditorsBatchCount(gi); + for (uint256 b = 0; b < n; b++) { + (, address[] memory auditors, , ) = ta.getAuditorsBatch(gi, b); + ta.assignAuditTestDataset(gi, b, hex"01", new bytes[](auditors.length), keccak256(abi.encode(gi, b))); + } + VM.clearMockedCalls(); +}