Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/scripts/check_task_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Verify task ABI bundles and deployable reference artifacts after forge build."""
from __future__ import annotations

import argparse
import hashlib
import json
from pathlib import Path

CONTRACTS = ("DINTaskCoordinator", "DINTaskAuditor")


def canonical_abi(abi: list) -> list[str]:
# ABI entry order is not semantically significant; preserve nested ordering.
return sorted(json.dumps(entry, sort_keys=True, separators=(",", ":")) for entry in abi)


def bytecode_diagnostics(bundled: object, expected: str, artifact: dict) -> str:
"""Expose reproducibility differences without relaxing exact bytecode parity."""
if not isinstance(bundled, str):
return "bundled creation bytecode is not a string"
actual = bytes.fromhex(expected.removeprefix("0x"))
try:
saved = bytes.fromhex(bundled.removeprefix("0x"))
except ValueError:
return "bundled creation bytecode is not valid hex"
if saved == actual:
difference = "decoded bytes identical; textual representation differs"
else:
first = next((i for i, (a, b) in enumerate(zip(saved, actual)) if a != b), min(len(saved), len(actual)))
difference = f"first differing byte: {first}"
metadata = artifact.get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except ValueError:
metadata = None
if not isinstance(metadata, dict):
details = "build metadata: unavailable"
else:
sources = metadata.get("sources", {})
source_hashes = {
path: source.get("keccak256")
for path, source in sources.items() if isinstance(source, dict)
} if isinstance(sources, dict) else {}
details = (
f"build compiler: {json.dumps(metadata.get('compiler', {}), sort_keys=True)}; "
f"build settings: {json.dumps(metadata.get('settings', {}), sort_keys=True)}; "
f"build source hashes: {json.dumps(source_hashes, sort_keys=True)}"
)
return (
f"{difference}; bundled length: {len(saved)}; build length: {len(actual)}; "
f"bundled sha256: {hashlib.sha256(saved).hexdigest()}; "
f"build sha256: {hashlib.sha256(actual).hexdigest()}; "
f"{details}"
)


def check_artifacts(root: Path) -> list[str]:
errors = []
for name in CONTRACTS:
artifact_path = root / "foundry/out" / f"{name}.sol" / f"{name}.json"
try:
artifact = json.loads(artifact_path.read_text())
expected_abi = canonical_abi(artifact["abi"])
expected_bytecode = artifact["bytecode"]["object"]
if not expected_bytecode or expected_bytecode == "0x":
raise ValueError("empty build bytecode")
except (OSError, ValueError, KeyError, TypeError) as exc:
errors.append(f"{name}: cannot read build artifact; run forge build first ({exc})")
continue

for directory in ("dincli/abis", "cache_model_0/abis"):
path = root / directory / f"{name}.json"
try:
bundled = json.loads(path.read_text())
if canonical_abi(bundled["abi"]) != expected_abi:
errors.append(f"{directory}/{name}.json: ABI differs from build")
if directory == "cache_model_0/abis" and bundled.get("bytecode") != expected_bytecode:
errors.append(f"{directory}/{name}.json: creation bytecode differs from build")
errors.append(f"{name}: {bytecode_diagnostics(bundled.get('bytecode'), expected_bytecode, artifact)}")
except (OSError, ValueError, KeyError, TypeError) as exc:
errors.append(f"{directory}/{name}.json: invalid artifact ({exc})")
return errors


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
args = parser.parse_args()
errors = check_artifacts(args.root)
for error in errors:
print(error)
if errors:
return 1
print("Task ABI bundles and reference creation bytecode match the Foundry build")
return 0


if __name__ == "__main__":
raise SystemExit(main())
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ jobs:
working-directory: foundry
run: forge build

- name: Verify task artifact bundles
run: python3 .github/scripts/check_task_artifacts.py

# EIP-170/EIP-3860 budget for every foundry/src contract: fails below a
# 1,024 B runtime margin, warns below 2,048 B. Reads artifacts directly
# because `forge build --sizes` omits DINTaskAuditor. Issue #201 Part A.
Expand Down
6 changes: 3 additions & 3 deletions Developer/BACK_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
| BL-28 | dincli (model owner) | No dincli command for `releaseGIRegistrationSlots` | `DINTaskCoordinator.releaseGIRegistrationSlots(gi)` (owner-only, once per ended GI) is the only thing that decrements validators' `activeRegistrationCount`; `endGI` deliberately doesn't (BL-10). dincli ships the function in its ABI but has no command that calls it, so under a non-zero `maxConcurrentRegistrationsPerStakeUnit` every GI permanently consumes its validators' concurrent-registration slots until they hit `TC_/TA_ConcurrentRegistrationCapReached`. Harmless while the cap is 0 (the default). | P3 (before the cap is set on any network) | task_100926_12 / issue #37 fast-follow; PR No. 204 review | 🟡 Fix in review — [PR #302](https://github.com/InfiniteZeroFoundation/DevNet/pull/302) adds `dincli model-owner gi release-slots <model_id> [--gi N]` (task_061026_21 Part C); tracked in [#223](https://github.com/InfiniteZeroFoundation/DevNet/issues/223) Part 2 |
| BL-29 | Solidity/foundry (fees) | `DinFeeRouter`'s non-treasury shares accrue with no way out | `routeFeeETH`/`routeFeeDIN` pay the treasury share out immediately but only *count* the `validatorPool`, `storage` and `publicGoods` shares in `accruedEth`/`accruedDin`. No function withdraws or distributes them, so with the default 95% validator-pool split, 95% of every swept faucet/registry fee stays in the router until an upgrade adds a path. Needs a decision on who can pull each bucket and where the validator-pool share goes (reward pools? `DinEmission`?). | P3/P4 | PR No. 204 review (`DinCoordinator.md` §14 No. 2), Sep 30, 2026; related to issue #43's open items | 🆕 New — unaddressed; tracked in [#221](https://github.com/InfiniteZeroFoundation/DevNet/issues/221) |
| BL-30 | Solidity/foundry (task contracts) | No recovery from a stalled GI | If a T1/T2 batch never reaches the reveal quorum, `finalizeT1Aggregation`/`finalizeT2Aggregation` revert forever (`TC_NoSubmissions`/`TC_InsufficientSubmissions`), and there is no owner path to skip the batch or abort the GI. `endGI` needs `AggregatorsSlashed` and `startGI` needs `GIended`, so the model is stuck: its reward pool and its validators' registration slots stay locked. This is also why BL-26's contract-side re-anchor cap was deferred. Needs a GI-abort design: who may abort, when, and where the reward pool goes. | P3/P4 | PR No. 191 review (BL-26); PR No. 204 review (`DINTaskCoordinator.md` §10 No. 6) | 🆕 Decided, unimplemented — Abraham's GI-abort policy (Oct 26, [#222](https://github.com/InfiniteZeroFoundation/DevNet/issues/222)): anyone may abort after a stall timeout, locked rewards go back to their funders, slots are released, non-revealers are still penalised; the timeout value is set with the other testnet parameters in [#155](https://github.com/InfiniteZeroFoundation/DevNet/issues/155). Adds code to `DINTaskCoordinator`, which has 55 B above the size gate ([#201](https://github.com/InfiniteZeroFoundation/DevNet/issues/201)) |
| BL-31 | Solidity/foundry (slashing) | S6 no-participation slashing is implemented but not wired | `DinValidatorStake.recordNoParticipation` (escalating 10%-per-breach slash past `s6NoParticipationThreshold`) exists and is tested, but no task contract calls it. They deliberately skip it where `slashPartial` (S1/S2, with S5 escalation) already penalises the same missed work, to avoid stacking past `MIN_STAKE`. So S6 never fires. Decide whether S6 should cover something S1/S2 don't (e.g. registered but never assigned or never active), or whether to remove it and its parameters. | P3 | PR No. 204 review (`DinValidatorStake.md` §14 No. 1), Sep 30, 2026; slashing taxonomy S6 (issue #38) | ⏸️ Parked for testnet — Abraham (Oct 26, [#224](https://github.com/InfiniteZeroFoundation/DevNet/issues/224)): S1/S2 plus S5 already cover missed work; leave S6 unwired (or remove it and its parameters) and revisit after testnet. Needs a removal PR or a "parked" note in the docs |
| BL-31 | Solidity/foundry (slashing) | S6 no-participation slashing is implemented but not wired | `DinValidatorStake.recordNoParticipation` (escalating 10%-per-breach slash past `s6NoParticipationThreshold`) exists and is tested, but no task contract calls it. They deliberately skip it where `slashPartial` (S1/S2, with S5 escalation) already penalises the same missed work, to avoid stacking past `MIN_STAKE`. So S6 never fires. Decide whether S6 should cover something S1/S2 don't (e.g. registered but never assigned or never active), or whether to remove it and its parameters. | P3 | PR No. 204 review (`DinValidatorStake.md` §14 No. 1), Sep 30, 2026; slashing taxonomy S6 (issue #38) | ⏸️ Parked for testnet — Abraham (Oct 26, [#224](https://github.com/InfiniteZeroFoundation/DevNet/issues/224)): S1/S2 plus S5 already cover missed work; leave S6 unwired (or remove it and its parameters) and revisit after testnet. Parked behavior documented in PR [#305](https://github.com/InfiniteZeroFoundation/DevNet/pull/305); storage, parameters and API remain unchanged |
| BL-32 | Tooling / onboarding | Contributor environment preflight check | No single command checks a dev machine before work starts. Each of these has already cost time: (1) Python ≥ 3.12; (2) `forge`/`anvil` installed and solc 0.8.28 available; (3) `foundry/node_modules` present — without `npm ci`, `forge test`'s parallel tests race to fetch `@openzeppelin/upgrades-core` over npx (see [CLAUDE.md](../CLAUDE.md)); (4) `eth-account` and `py-multibase` import; (5) `torch` imports in the active venv (full `pytest` run); (6) `.env`/`.env.<network>` exist, the RPC endpoint answers, and `resolve_ipfs_config()` resolves. | P3/P4 (onboarding; small, ~half a day) | `preflight.sh` pattern from [Osomudeya/devops-scripting-labs](https://github.com/Osomudeya/devops-scripting-labs) (reviewed 2026-10-02); no existing equivalent (note: the only "preflight" in `dincli/cli/system.py` is the bridge's tx preflight — unrelated) | 🟡 Mostly fixed — [PR #255](https://github.com/InfiniteZeroFoundation/DevNet/pull/255) added `scripts/preflight.py` (Python/forge/anvil/node_modules/package checks, `--network` RPC/IPFS checks, 13 tests in `tests/test_preflight.py`, a preflight section in `Documentation/public/getting-started.md`). Remaining (exit non-zero on `node_modules`, eth-account/py-multibase, `--network` RPC/IPFS failures) tracked in [#245](https://github.com/InfiniteZeroFoundation/DevNet/issues/245) |
| BL-33 | Tooling / deploy | On-chain deployment drift check | Nothing verifies that `foundry/deployments/<network>.json` (and the `din_info` that `dincli system import-deployments` writes) still matches the chain. Proposal: a read-only check, either a forge script or `dincli system verify-deployment`, that for each recorded contract compares (1) the proxy's EIP-1967 implementation and admin slots against the recorded implementation/`proxyAdmin*` addresses; (2) the deployed runtime code hash against the current `forge build` artifact (immutables masked); (3) owner / DIN-Representative addresses; (4) wiring: `DinCoordinator` ↔ `DinToken`/`DinValidatorStake`/`DinModelRegistry`/fee router/treasury links and slasher authorizations on `DinValidatorStake`. Report per-item `OK` / `DRIFT` with expected vs. actual. Caveat: it only covers what the deployments JSON records, so a clean run doesn't prove nothing else was deployed or authorized. | P3 (before the DevNet 3.0 deploy; re-run after every `UpgradePlatform.s.sol` run) | Terraform state-vs-live drift pattern from [Osomudeya/devops-scripting-labs](https://github.com/Osomudeya/devops-scripting-labs) `03-drift-detection` (reviewed 2026-10-02); no existing code reads EIP-1967 slots or code hashes in `dincli/` or `foundry/script/` | ✅ Fixed — PR [#254](https://github.com/InfiniteZeroFoundation/DevNet/pull/254) (issue [#244](https://github.com/InfiniteZeroFoundation/DevNet/issues/244)). `dincli system verify-deployment` added: reads the deployments JSON, checks EIP-1967 impl/admin slots, extcodehash, ProxyAdmin owner(), DinCoordinator wiring, and slasher authorizations. `DeployPlatform.s.sol` and `UpgradePlatform.s.sol` extended to write impl addresses, codehashes, deployer, and git commit. 16 tests in `tests/test_verify_deployment.py`. |
| BL-34 | Solidity/foundry (task contracts) | `DINModelRegistry.disableModel()` kill-switch doesn't reach the live task contracts | `modelDisabled[modelId]` only gates `requestModelRegistration`/`requestManifestUpdate` inside the registry itself. `DINTaskCoordinator`/`DINTaskAuditor` have no dependency on, or awareness of, the registry (confirmed: zero functional references; the auditor has two doc comments naming `DINModelRegistry` by name (L31, L434) plus two more referencing "model registry" generically without naming the type (L393, L405) — none are functional checks). A model the DIN-Representative has disabled — e.g. because its task contracts are wrongfully slashing honest validators — keeps running full GIs completely unaffected. The task contracts' own doc table calls this a "kill-switch," which it isn't. Needs a decision: wire the task contracts to check `modelDisabled` before state-changing calls (requires threading the model ID + registry address into contracts that don't know about the registry today), or document `disableModel` as registry-metadata-only. | P3/P4 | 2026-07 security review M-2 (`Documentation/technical/audits/foundry-src-security-review.md`) | 🆕 Decided, unimplemented — Abraham (Oct 26, [#224](https://github.com/InfiniteZeroFoundation/DevNet/issues/224)): make the kill-switch real before the public testnet (at minimum stop new GIs on a disabled model's task contracts), preferably in `DINTaskAuditor` or a small external guard given `DINTaskCoordinator`'s size headroom |
| BL-35 | Solidity/foundry (registry) | `rejectModel()`'s non-refund of the registration fee was never confirmed as intentional | `feePaid` (set at `requestModelRegistration`) is never refunded on `rejectModel()`, but it's also never refunded on `approveModel()` either — the fee appears to be a deliberate pay-to-apply cost regardless of outcome (anti-spam), not an oversight specific to rejection. Never written down as a deliberate choice anywhere. Needs confirmation and a doc note in `DINModelRegistry.md` / the dinrep role docs, so a future reader doesn't "fix" it into an inconsistent partial-refund state. | P4 (doc-only once confirmed) | 2026-07 security review L-4 (`Documentation/technical/audits/foundry-src-security-review.md`) | ✅ Decided — keep as intended (Abraham, Oct 26, [#224](https://github.com/InfiniteZeroFoundation/DevNet/issues/224)): the registration fee is a pay-to-apply anti-spam cost, refunded on neither approval nor rejection. Docs pending (`DINModelRegistry.md`, dinrep role docs) |
| BL-34 | Solidity/foundry (task contracts) | `DINModelRegistry.disableModel()` kill-switch doesn't reach the live task contracts | `modelDisabled[modelId]` only gates `requestModelRegistration`/`requestManifestUpdate` inside the registry itself. `DINTaskCoordinator`/`DINTaskAuditor` have no dependency on, or awareness of, the registry (confirmed: zero functional references; the auditor has two doc comments naming `DINModelRegistry` by name (L31, L434) plus two more referencing "model registry" generically without naming the type (L393, L405) — none are functional checks). A model the DIN-Representative has disabled — e.g. because its task contracts are wrongfully slashing honest validators — keeps running full GIs completely unaffected. The task contracts' own doc table calls this a "kill-switch," which it isn't. Needs a decision: wire the task contracts to check `modelDisabled` before state-changing calls (requires threading the model ID + registry address into contracts that don't know about the registry today), or document `disableModel` as registry-metadata-only. | P3/P4 | 2026-07 security review M-2 (`Documentation/technical/audits/foundry-src-security-review.md`) | ✅ Resolved — `validateGIStart` wired in `DINTaskAuditor.sol`; both `startGI` overloads check registry approval and disabled status via `_startGI`. Compatible task pairs required; existing deployed pairs unaffected. [PR No. 305](https://github.com/InfiniteZeroFoundation/DevNet/pull/305). |
| BL-35 | Solidity/foundry (registry) | `rejectModel()`'s non-refund of the registration fee was never confirmed as intentional | `feePaid` (set at `requestModelRegistration`) is never refunded on `rejectModel()`, but it's also never refunded on `approveModel()` either — the fee appears to be a deliberate pay-to-apply cost regardless of outcome (anti-spam), not an oversight specific to rejection. Never written down as a deliberate choice anywhere. Needs confirmation and a doc note in `DINModelRegistry.md` / the dinrep role docs, so a future reader doesn't "fix" it into an inconsistent partial-refund state. | P4 (doc-only once confirmed) | 2026-07 security review L-4 (`Documentation/technical/audits/foundry-src-security-review.md`) | ✅ Decided — keep as intended (Abraham, Oct 26, [#224](https://github.com/InfiniteZeroFoundation/DevNet/issues/224)): the registration fee is a pay-to-apply anti-spam cost, refunded on neither approval nor rejection. Documented in `DINModelRegistry.md` and dinrep role docs by PR [#305](https://github.com/InfiniteZeroFoundation/DevNet/pull/305) |
4 changes: 2 additions & 2 deletions Documentation/public/roles/dinrep.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ dincli dinrep registry approve-registration-request <requestId>
dincli dinrep registry reject-registration-request <requestId>
```

The registration fee is retained by the contract in both cases.
The required registration fee is a nonrefundable, pay-to-apply anti-spam cost: neither approval nor rejection refunds it. This is intentional (Abraham, [issue #224, 26 October 2026](https://github.com/InfiniteZeroFoundation/DevNet/issues/224#issuecomment-6406239550)). Any amount paid above the required fee is still refunded in the request transaction.

---

Expand Down Expand Up @@ -203,7 +203,7 @@ dincli dinrep registry enable-model <modelId>
```

> [!CAUTION]
> Disabling a model does not delete it. All on-chain history is preserved. Today the task contracts (`DINTaskCoordinator`, `DINTaskAuditor`) do **not** check `modelDisabled(modelId)`, so disabling only blocks manifest updates. It does not stop the model's running GIs, submissions or slashing.
> Disabling a model does not delete it. All on-chain history is preserved. With the current compatible Foundry task pair and canonical registry wiring, disabling blocks both new-GI start overloads, even when already funded, as well as manifest updates and emission funding. Registry approval is required before any GI starts. In-flight GIs, including slashing, settlement and claims, continue; this is not a GI abort. Existing deployed pairs are unchanged and require redeployment for the new guard. Review task code, pairing and canonical registry wiring before authorization/approval; immutable wiring alone does not establish trust. Reenable restores start eligibility. Pre-funded pools have no new refund or recovery path.

---

Expand Down
Loading
Loading