From f3d4486888e320cdfbcc994ef28af8e36341a78b Mon Sep 17 00:00:00 2001 From: FlowmemoryAI <283694809+FlowmemoryAI@users.noreply.github.com> Date: Wed, 13 May 2026 16:02:30 -0500 Subject: [PATCH] Harden FlowChain settlement spine --- contracts/ACCESS_CONTROL_REVIEW.md | 15 +- contracts/ArtifactRegistry.sol | 2 + contracts/DEPLOYMENT_BOUNDARY.md | 56 +++ contracts/FLOWPULSE_SCHEMA.md | 8 +- contracts/ReceiptVerifier.sol | 4 + contracts/RootfieldRegistry.sol | 8 + tests/LiveV0Package.t.sol | 530 +++++++++++++++++++++++++++-- tests/README.md | 9 + tests/RootfieldRegistry.t.sol | 54 +++ 9 files changed, 651 insertions(+), 35 deletions(-) diff --git a/contracts/ACCESS_CONTROL_REVIEW.md b/contracts/ACCESS_CONTROL_REVIEW.md index 48b6298e..c4de6767 100644 --- a/contracts/ACCESS_CONTROL_REVIEW.md +++ b/contracts/ACCESS_CONTROL_REVIEW.md @@ -6,6 +6,10 @@ Status: V0 launch hardening review. The current contracts use simple ownership or self-registration patterns. They do not implement staking, slashing, token custody, rewards, production governance, verifier consensus, or upgrade admin controls. +They also do not enforce cross-contract dependency existence. For example, a work receipt or verifier report may reference a nonzero `rootfieldId` or `receiptId` that another contract has not registered. That is intentional for this optional V0 event spine: indexers and verifiers reconcile dependencies off-chain from receipts, logs, fixtures, and reports. + +No current contract exposes bridge finality or a challenge lifecycle. `REORGED` is an allowed verifier-report status for local/test reconciliation, not a Solidity finality proof, production bridge state, or challenge-resolution mechanism. + ## RootfieldRegistry Owner model: each `rootfieldId` has one owner. @@ -19,8 +23,10 @@ Owner-gated functions: Current protections: - zero rootfield id rejected +- zero schema hash rejected - duplicate rootfield id rejected - zero root rejected +- zero artifact commitment rejected for root submissions - inactive rootfield blocks root submission and transfer - zero new owner rejected - ownership transfer emits both a FlowPulse status event and a dedicated ownership event @@ -50,9 +56,10 @@ Submitter-gated functions: Current protections: - zero worker/verifier rejected +- revoked worker/verifier authorization blocks future submissions - duplicate report/receipt id rejected -- invalid report status rejected -- invalid work lane rejected +- invalid report status rejected below and above the accepted V0 range +- invalid work lane rejected below and above the accepted V0 range - zero target or commitment fields rejected Launch risk to watch: @@ -96,6 +103,8 @@ Owner-gated functions: Current protections: - zero ids and zero commitments rejected +- zero rootfield id rejected where a record belongs to a Rootfield namespace +- zero artifact schema hash rejected - duplicate records rejected - only the stored owner can mutate the record @@ -113,7 +122,7 @@ Contracts: Current boundary: -- `ReceiptVerifier` accepts first-writer receipt-report commitments and does not cryptographically verify receipts. +- `ReceiptVerifier` accepts first-writer receipt-report commitments and does not cryptographically verify receipts. It rejects zero report ids, observation ids, rootfield ids, receipt commitments, and report hashes so local-alpha reports remain reconstructable by indexers/verifiers. - `WorkDebtScheduler` allows any scheduler to assign work to a nonzero worker and allows scheduler or worker to mark completion. - `FlowMemoryHookAdapter` validates nonzero inputs and emits an observation event. It also exposes a dependency-light Uniswap v4-shaped `afterSwap` callback path, but it is not a production Uniswap v4 hook deployment. diff --git a/contracts/ArtifactRegistry.sol b/contracts/ArtifactRegistry.sol index 13191e5f..3d1040e9 100644 --- a/contracts/ArtifactRegistry.sol +++ b/contracts/ArtifactRegistry.sol @@ -13,6 +13,7 @@ contract ArtifactRegistry is IArtifactRegistry { error ZeroRootfieldId(); error ZeroArtifactType(); error ZeroCommitmentHash(); + error ZeroSchemaHash(); error ArtifactAlreadyRegistered(bytes32 artifactId); error ArtifactNotRegistered(bytes32 artifactId); error ArtifactNotActive(bytes32 artifactId); @@ -32,6 +33,7 @@ contract ArtifactRegistry is IArtifactRegistry { if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); if (artifactType == bytes32(0)) revert ZeroArtifactType(); if (commitmentHash == bytes32(0)) revert ZeroCommitmentHash(); + if (schemaHash == bytes32(0)) revert ZeroSchemaHash(); if (_artifacts[artifactId].exists) revert ArtifactAlreadyRegistered(artifactId); uint64 now64 = _blockTimestamp(); diff --git a/contracts/DEPLOYMENT_BOUNDARY.md b/contracts/DEPLOYMENT_BOUNDARY.md index e3b6ce81..f0ad5499 100644 --- a/contracts/DEPLOYMENT_BOUNDARY.md +++ b/contracts/DEPLOYMENT_BOUNDARY.md @@ -2,12 +2,17 @@ Status: V0 local and Base Sepolia readiness boundary. +The current contracts are a compact event and commitment spine. They store intentional roots, receipt/report commitments, registry metadata hashes, counters, and status fields only. Heavy artifacts, AI memory, media, model data, verifier evidence, and receipt reconstruction data remain off-chain. + +For the private/local FlowChain testnet package, these Solidity contracts are optional settlement/event anchors. They are not the private L1 runtime. The private/local runtime remains the Rust/local devnet and local services path; Solidity may mirror compact events or commitments for tests and canaries only when that boundary is explicit. + ## Allowed Now - Local Foundry tests. - Local fixture generation and indexer/verifier/dashboard flows. - Base Sepolia deployment dry runs and explicit broadcasts for the current V0 contracts. - Base Sepolia reads from explicit RPC URLs. +- Guarded Base mainnet canary reads and source-verification dry runs for the documented V0 canary addresses only. - Public docs that describe emitted events, roots, receipts, and off-chain verification paths. ## Not Allowed Yet @@ -15,12 +20,63 @@ Status: V0 local and Base Sepolia readiness boundary. - Base mainnet deployment claims. - Production-mainnet readiness claims. - Production L1 claims. +- Claims that the Solidity contracts are the private/local FlowChain L1 runtime. +- Production Base settlement-anchor claims. +- Production bridge, production finality, or production challenge-resolution claims. +- Broad Base mainnet scans outside the documented canary reader guardrails. - Token launch, rewards, slashing, or fee-market mechanics. - Dynamic Uniswap v4 fee hooks. - Custody of user tokens. - Claims that contracts can know `txHash` or `logIndex` during execution. - Claims that on-chain storage is free or that arbitrary AI data is stored on-chain. +## Settlement Anchor Boundary + +Base anchoring is placeholder/research until separately approved. A future anchor must be scoped in its own issue or decision record with threat model, source/target chain assumptions, replay boundaries, event semantics, indexer/verifier responsibilities, and deployment review. The current V0 contracts do not implement a bridge, production settlement finality, token movement, or appchain/L1 launch path. + +FlowPulse events intentionally omit `txHash` and `logIndex`; indexers derive those values after receipts and logs exist. URI fields are advisory caller-supplied log data unless a future contract explicitly validates format, length, resolvability, or content hash linkage. + +No current Solidity contract exposes a challenge lifecycle or finality state machine. `VerifierReportRegistry.REORGED` is an advisory report status for off-chain reconciliation, not a bridge finality proof or challenge resolution path. + +## Private/Local FlowChain Mirror Map + +The private/local FlowChain runtime owns object execution and final state. The Solidity spine may mirror or anchor only compact object references: + +| Private/local object | Optional Solidity mirror | +| --- | --- | +| Agent/operator identity | `WorkerRegistry` metadata commitments | +| Verifier module identity | `VerifierRegistry` metadata commitments | +| WorkReceipt | `WorkReceiptRegistry` compact receipt commitments | +| VerifierReport | `VerifierReportRegistry` or `ReceiptVerifier` report commitments | +| ArtifactAvailabilityProof or model metadata pointer | `ArtifactRegistry` commitment and schema hashes | +| Indexer checkpoint | `CursorRegistry` cursor commitments | +| MemoryCell or Rootflow state update | `RootfieldRegistry` root commitments and FlowPulse events | +| Challenge or finality state | Not mirrored by Solidity V0; handled by the Rust/local devnet and local services | + +These mirrors do not make Solidity the private L1 runtime and do not create production bridge, settlement, fee, token, or validator semantics. + +## Local Hardening Commands + +Run these from the repository root before review: + +```powershell +forge test +npm run contracts:hardening +git diff --check +``` + +`npm run contracts:hardening` runs the local Foundry hardening baseline and Slither when it is installed. Slither can be made mandatory with: + +```powershell +npm run contracts:hardening:slither +``` + +Formatting can be checked explicitly with: + +```powershell +.\infra\scripts\contracts-static-analysis.ps1 -CheckFormat +``` + ## Deployment Inputs Required Before a Base Sepolia deployment transaction is sent, the PR or issue must record: diff --git a/contracts/FLOWPULSE_SCHEMA.md b/contracts/FLOWPULSE_SCHEMA.md index 893b4204..77cb9b21 100644 --- a/contracts/FLOWPULSE_SCHEMA.md +++ b/contracts/FLOWPULSE_SCHEMA.md @@ -1,6 +1,8 @@ # FlowPulse Schema v0 -FlowPulse is the first shared event stream for FlowMemory protocol activity. It is intentionally small and commitment-oriented: contracts emit roots, commitments, and advisory URI strings while indexers and verifiers reconstruct full context from receipts, logs, and off-chain artifacts. +FlowPulse is the first shared event stream for FlowMemory protocol activity. It is intentionally small and commitment-oriented: contracts emit roots, commitments, and advisory URI strings while indexers and verifiers reconstruct full context from receipts, logs, and off-chain artifacts. Contract state is compact by design; it is not an artifact store, model store, bridge state machine, or production L1 state surface. + +For the private/local FlowChain testnet, FlowPulse contracts are optional anchors and event mirrors. The private L1 runtime is the Rust/local devnet and local service stack, not Solidity. ## Solidity Event @@ -42,8 +44,12 @@ FlowPulse does not include `txHash` or `logIndex`. Those values are not availabl - `pulseId` is unique within the emitting contract's domain but indexers should still key canonical observations by chain id, contract address, transaction hash, and log index. - `uri` values are advisory by convention only. The skeleton does not enforce that they are short, resolvable, or off-chain pointers, so callers and reviewers must treat the off-chain-data boundary as a design convention rather than an enforcement guarantee. +- `RootfieldRegistry` rejects zero `rootfieldId`, zero `schemaHash`, zero committed root, and zero `artifactCommitment` so local-alpha root transitions keep enough compact state for indexers and verifiers to reconstruct the object model. - Verifiers must validate any referenced off-chain content against the emitted `commitment`. - Pulse type expansion should happen by reserving new numeric values and documenting their subject and commitment semantics before contracts depend on them. +- Base settlement-anchor use remains placeholder/research until a separate issue, threat model, and deployment review approve a concrete anchor design. The current event schema does not implement a bridge, appchain finality, or production settlement guarantees. +- `FlowMemoryHookAdapter` is a V0 scaffold. Its direct helper and dependency-light Uniswap v4-shaped `afterSwap` path emit the same `SWAP_MEMORY_SIGNAL` semantics, but neither path is a production hook deployment, dynamic-fee hook, or custody path. +- Current Solidity contracts do not expose challenge resolution or finality state. Verifier statuses such as `REORGED` are compact report values for off-chain reconciliation. ## Current Pulse Types diff --git a/contracts/ReceiptVerifier.sol b/contracts/ReceiptVerifier.sol index 4ec93ff5..e0ac2b0e 100644 --- a/contracts/ReceiptVerifier.sol +++ b/contracts/ReceiptVerifier.sol @@ -13,7 +13,9 @@ contract ReceiptVerifier is IReceiptVerifier { error ZeroReportId(); error ZeroObservationId(); + error ZeroRootfieldId(); error ZeroReceiptCommitment(); + error ZeroReportHash(); error ReceiptReportAlreadySubmitted(bytes32 reportId); error TimestampOverflow(uint256 timestamp); @@ -27,7 +29,9 @@ contract ReceiptVerifier is IReceiptVerifier { ) external { if (reportId == bytes32(0)) revert ZeroReportId(); if (observationId == bytes32(0)) revert ZeroObservationId(); + if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); if (receiptCommitment == bytes32(0)) revert ZeroReceiptCommitment(); + if (reportHash == bytes32(0)) revert ZeroReportHash(); if (_reports[reportId].status != ReceiptStatus.Unknown) revert ReceiptReportAlreadySubmitted(reportId); _reports[reportId] = ReceiptReport({ diff --git a/contracts/RootfieldRegistry.sol b/contracts/RootfieldRegistry.sol index c82bdf49..ecb8eaec 100644 --- a/contracts/RootfieldRegistry.sol +++ b/contracts/RootfieldRegistry.sol @@ -22,7 +22,9 @@ contract RootfieldRegistry is IFlowPulse, IRootfieldRegistry { mapping(bytes32 rootfieldId => Rootfield rootfield) private _rootfields; error ZeroRootfieldId(); + error ZeroSchemaHash(); error ZeroRoot(); + error ZeroArtifactCommitment(); error RootfieldAlreadyRegistered(bytes32 rootfieldId); error RootfieldNotRegistered(bytes32 rootfieldId); error RootfieldInactive(bytes32 rootfieldId); @@ -46,6 +48,9 @@ contract RootfieldRegistry is IFlowPulse, IRootfieldRegistry { if (rootfieldId == bytes32(0)) { revert ZeroRootfieldId(); } + if (schemaHash == bytes32(0)) { + revert ZeroSchemaHash(); + } if (_rootfields[rootfieldId].owner != address(0)) { revert RootfieldAlreadyRegistered(rootfieldId); } @@ -85,6 +90,9 @@ contract RootfieldRegistry is IFlowPulse, IRootfieldRegistry { if (root == bytes32(0)) { revert ZeroRoot(); } + if (artifactCommitment == bytes32(0)) { + revert ZeroArtifactCommitment(); + } rootfield.latestRoot = root; rootfield.rootCount += 1; diff --git a/tests/LiveV0Package.t.sol b/tests/LiveV0Package.t.sol index 5e4041d9..cb2252d1 100644 --- a/tests/LiveV0Package.t.sol +++ b/tests/LiveV0Package.t.sol @@ -122,6 +122,31 @@ contract LiveV0PackageTest { _assertTrue(cursor.active); } + function testCursorRegistryRejectsZeroIdsAndCommitments() public { + CursorRegistry registry = new CursorRegistry(); + + vm.expectRevert(CursorRegistry.ZeroCursorId.selector); + registry.registerCursor( + bytes32(0), keccak256("stream.zero"), keccak256("position.1"), keccak256("metadata"), "" + ); + + vm.expectRevert(CursorRegistry.ZeroStreamId.selector); + registry.registerCursor( + keccak256("cursor.zero-stream"), bytes32(0), keccak256("position.1"), keccak256("metadata"), "" + ); + + vm.expectRevert(CursorRegistry.ZeroPositionCommitment.selector); + registry.registerCursor( + keccak256("cursor.zero-position"), keccak256("stream.zero-position"), bytes32(0), keccak256("metadata"), "" + ); + + bytes32 cursorId = keccak256("cursor.advance-zero"); + registry.registerCursor(cursorId, keccak256("stream.advance-zero"), keccak256("position.1"), bytes32(0), ""); + + vm.expectRevert(CursorRegistry.ZeroPositionCommitment.selector); + registry.advanceCursor(cursorId, bytes32(0), keccak256("metadata.2"), ""); + } + function testCursorRegistryRejectsDuplicateAndNonOwnerAdvance() public { CursorRegistry registry = new CursorRegistry(); bytes32 cursorId = keccak256("cursor.beta"); @@ -133,6 +158,10 @@ contract LiveV0PackageTest { CursorRegistryCaller caller = new CursorRegistryCaller(); vm.expectRevert(abi.encodeWithSelector(CursorRegistry.NotCursorOwner.selector, cursorId, address(caller))); caller.advanceCursor(registry, cursorId, keccak256("position.2")); + + bytes32 missingCursorId = keccak256("cursor.missing"); + vm.expectRevert(abi.encodeWithSelector(CursorRegistry.CursorNotRegistered.selector, missingCursorId)); + registry.advanceCursor(missingCursorId, keccak256("position.2"), keccak256("metadata"), ""); } function testWorkerAndVerifierRegistriesStoreSelfRegisteredMetadata() public { @@ -167,6 +196,38 @@ contract LiveV0PackageTest { _assertTrue(verifier.active); } + function testWorkerAndVerifierRegistriesRejectDuplicateAndZeroFields() public { + WorkerRegistry workers = new WorkerRegistry(); + VerifierRegistry verifiers = new VerifierRegistry(); + + vm.expectRevert(WorkerRegistry.ZeroOperatorId.selector); + workers.registerWorker(bytes32(0), keccak256("worker.role"), keccak256("worker.metadata"), ""); + + vm.expectRevert(WorkerRegistry.ZeroWorkerRole.selector); + workers.registerWorker(keccak256("worker.operator"), bytes32(0), keccak256("worker.metadata"), ""); + + vm.expectRevert(VerifierRegistry.ZeroOperatorId.selector); + verifiers.registerVerifier(bytes32(0), keccak256("verifier.role"), keccak256("verifier.metadata"), ""); + + vm.expectRevert(VerifierRegistry.ZeroVerifierRole.selector); + verifiers.registerVerifier(keccak256("verifier.operator"), bytes32(0), keccak256("verifier.metadata"), ""); + + workers.registerWorker(keccak256("worker.operator"), keccak256("worker.role"), keccak256("worker.metadata"), ""); + verifiers.registerVerifier( + keccak256("verifier.operator"), keccak256("verifier.role"), keccak256("verifier.metadata"), "" + ); + + vm.expectRevert(abi.encodeWithSelector(WorkerRegistry.WorkerAlreadyRegistered.selector, address(this))); + workers.registerWorker( + keccak256("worker.operator.v2"), keccak256("worker.role.v2"), keccak256("worker.metadata.v2"), "" + ); + + vm.expectRevert(abi.encodeWithSelector(VerifierRegistry.VerifierAlreadyRegistered.selector, address(this))); + verifiers.registerVerifier( + keccak256("verifier.operator.v2"), keccak256("verifier.role.v2"), keccak256("verifier.metadata.v2"), "" + ); + } + function testWorkerAndVerifierRegistriesDeactivateAndRejectUnregisteredUpdates() public { WorkerRegistry workers = new WorkerRegistry(); VerifierRegistry verifiers = new VerifierRegistry(); @@ -244,6 +305,39 @@ contract LiveV0PackageTest { ArtifactRegistry registry = new ArtifactRegistry(); bytes32 artifactId = keccak256("artifact.beta"); + vm.expectRevert(ArtifactRegistry.ZeroArtifactId.selector); + registry.registerArtifact( + bytes32(0), + keccak256("rootfield.beta"), + keccak256("artifact.type"), + keccak256("artifact.commitment"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(ArtifactRegistry.ZeroRootfieldId.selector); + registry.registerArtifact( + artifactId, + bytes32(0), + keccak256("artifact.type"), + keccak256("artifact.commitment"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(ArtifactRegistry.ZeroArtifactType.selector); + registry.registerArtifact( + artifactId, + keccak256("rootfield.beta"), + bytes32(0), + keccak256("artifact.commitment"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + vm.expectRevert(ArtifactRegistry.ZeroCommitmentHash.selector); registry.registerArtifact( artifactId, @@ -255,6 +349,17 @@ contract LiveV0PackageTest { "" ); + vm.expectRevert(ArtifactRegistry.ZeroSchemaHash.selector); + registry.registerArtifact( + artifactId, + keccak256("rootfield.beta"), + keccak256("artifact.type"), + keccak256("artifact.commitment"), + bytes32(0), + keccak256("metadata.hash"), + "" + ); + registry.registerArtifact( artifactId, keccak256("rootfield.beta"), @@ -281,6 +386,10 @@ contract LiveV0PackageTest { ArtifactRegistry registry = new ArtifactRegistry(); ArtifactRegistryCaller caller = new ArtifactRegistryCaller(); bytes32 artifactId = keccak256("artifact.gamma"); + bytes32 missingArtifactId = keccak256("artifact.missing"); + + vm.expectRevert(abi.encodeWithSelector(ArtifactRegistry.ArtifactNotRegistered.selector, missingArtifactId)); + registry.deprecateArtifact(missingArtifactId, keccak256("artifact.deprecated"), ""); registry.registerArtifact( artifactId, @@ -330,6 +439,60 @@ contract LiveV0PackageTest { _assertTrue(signatureWithReceiptMetadata != signatureWithoutReceiptMetadata); } + function testReceiptVerifierRejectsInvalidZeroFields() public { + ReceiptVerifier verifier = new ReceiptVerifier(); + + vm.expectRevert(ReceiptVerifier.ZeroReportId.selector); + verifier.submitReceiptReport( + bytes32(0), + keccak256("observation.id"), + keccak256("rootfield.alpha"), + keccak256("receipt.commitment"), + keccak256("report.hash"), + "" + ); + + vm.expectRevert(ReceiptVerifier.ZeroObservationId.selector); + verifier.submitReceiptReport( + keccak256("report.zero-observation"), + bytes32(0), + keccak256("rootfield.alpha"), + keccak256("receipt.commitment"), + keccak256("report.hash"), + "" + ); + + vm.expectRevert(ReceiptVerifier.ZeroRootfieldId.selector); + verifier.submitReceiptReport( + keccak256("report.zero-rootfield"), + keccak256("observation.id"), + bytes32(0), + keccak256("receipt.commitment"), + keccak256("report.hash"), + "" + ); + + vm.expectRevert(ReceiptVerifier.ZeroReceiptCommitment.selector); + verifier.submitReceiptReport( + keccak256("report.zero-receipt"), + keccak256("observation.id"), + keccak256("rootfield.alpha"), + bytes32(0), + keccak256("report.hash"), + "" + ); + + vm.expectRevert(ReceiptVerifier.ZeroReportHash.selector); + verifier.submitReceiptReport( + keccak256("report.zero-report-hash"), + keccak256("observation.id"), + keccak256("rootfield.alpha"), + keccak256("receipt.commitment"), + bytes32(0), + "" + ); + } + function testReceiptVerifierRejectsDuplicateReport() public { ReceiptVerifier verifier = new ReceiptVerifier(); bytes32 reportId = keccak256("report.dup"); @@ -375,6 +538,71 @@ contract LiveV0PackageTest { _assertTrue(item.status == IWorkDebtScheduler.WorkStatus.Completed); } + function testWorkDebtSchedulerRejectsZeroFieldsDuplicateAndCompletedTransition() public { + WorkDebtScheduler scheduler = new WorkDebtScheduler(); + bytes32 workId = keccak256("work.invalid"); + + vm.expectRevert(WorkDebtScheduler.ZeroWorkId.selector); + scheduler.scheduleWork( + bytes32(0), + address(this), + keccak256("rootfield.invalid"), + keccak256("work.commitment"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(WorkDebtScheduler.ZeroWorker.selector); + scheduler.scheduleWork( + workId, + address(0), + keccak256("rootfield.invalid"), + keccak256("work.commitment"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(WorkDebtScheduler.ZeroRootfieldId.selector); + scheduler.scheduleWork( + workId, address(this), bytes32(0), keccak256("work.commitment"), keccak256("metadata.hash"), "" + ); + + vm.expectRevert(WorkDebtScheduler.ZeroWorkCommitment.selector); + scheduler.scheduleWork( + workId, address(this), keccak256("rootfield.invalid"), bytes32(0), keccak256("metadata.hash"), "" + ); + + vm.expectRevert(abi.encodeWithSelector(WorkDebtScheduler.WorkNotScheduled.selector, workId)); + scheduler.markWorkComplete(workId, keccak256("completion"), keccak256("metadata.done"), ""); + + scheduler.scheduleWork( + workId, + address(this), + keccak256("rootfield.invalid"), + keccak256("work.commitment"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(abi.encodeWithSelector(WorkDebtScheduler.WorkAlreadyScheduled.selector, workId)); + scheduler.scheduleWork( + workId, + address(this), + keccak256("rootfield.invalid"), + keccak256("work.commitment.v2"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(WorkDebtScheduler.ZeroCompletionCommitment.selector); + scheduler.markWorkComplete(workId, bytes32(0), keccak256("metadata.done"), ""); + + scheduler.markWorkComplete(workId, keccak256("completion"), keccak256("metadata.done"), ""); + + vm.expectRevert(abi.encodeWithSelector(WorkDebtScheduler.WorkNotScheduledStatus.selector, workId)); + scheduler.markWorkComplete(workId, keccak256("completion.again"), keccak256("metadata.done.again"), ""); + } + function testWorkDebtSchedulerRejectsNonParticipantCompletion() public { WorkDebtScheduler scheduler = new WorkDebtScheduler(); bytes32 workId = keccak256("work.beta"); @@ -428,6 +656,42 @@ contract LiveV0PackageTest { _assertTrue(logs[0].emitter == address(registry)); } + function testWorkReceiptRegistryRejectsNonOwnerWorkerAuthorization() public { + WorkReceiptRegistry registry = new WorkReceiptRegistry(); + WorkReceiptRegistryCaller caller = new WorkReceiptRegistryCaller(); + + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.NotOwner.selector, address(caller))); + caller.setWorkerAuthorization(registry, address(this), true); + + vm.expectRevert(WorkReceiptRegistry.ZeroWorker.selector); + registry.setWorkerAuthorization(address(0), true); + } + + function testWorkReceiptRegistryBlocksRevokedWorker() public { + WorkReceiptRegistry registry = new WorkReceiptRegistry(); + bytes32 receiptId = keccak256("receipt.revoked"); + uint8 lane = registry.MEMORY_REFRESH(); + + registry.setWorkerAuthorization(address(this), true); + _assertTrue(registry.isAuthorizedWorker(address(this))); + + registry.setWorkerAuthorization(address(this), false); + _assertTrue(!registry.isAuthorizedWorker(address(this))); + + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.WorkerNotAuthorized.selector, address(this))); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.revoked"), + lane, + keccak256("subject.revoked"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + } + function testWorkReceiptRegistryRejectsUnauthorizedInvalidLaneAndZeroRoots() public { WorkReceiptRegistry registry = new WorkReceiptRegistry(); bytes32 receiptId = keccak256("receipt.beta"); @@ -449,6 +713,45 @@ contract LiveV0PackageTest { registry.setWorkerAuthorization(address(this), true); + vm.expectRevert(WorkReceiptRegistry.ZeroReceiptId.selector); + registry.submitWorkReceipt( + bytes32(0), + keccak256("rootfield.beta"), + memoryRefreshLane, + keccak256("subject.beta"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + + vm.expectRevert(WorkReceiptRegistry.ZeroRootfieldId.selector); + registry.submitWorkReceipt( + receiptId, + bytes32(0), + memoryRefreshLane, + keccak256("subject.beta"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.InvalidWorkLane.selector, 0)); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.beta"), + 0, + keccak256("subject.beta"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.InvalidWorkLane.selector, 9)); registry.submitWorkReceipt( receiptId, @@ -474,6 +777,32 @@ contract LiveV0PackageTest { bytes32(0), "" ); + + vm.expectRevert(WorkReceiptRegistry.ZeroOutputRoot.selector); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.beta"), + failureDiscoveryLane, + keccak256("subject.beta"), + keccak256("input.root"), + bytes32(0), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + + vm.expectRevert(WorkReceiptRegistry.ZeroArtifactCommitment.selector); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.beta"), + failureDiscoveryLane, + keccak256("subject.beta"), + keccak256("input.root"), + keccak256("output.root"), + bytes32(0), + bytes32(0), + "" + ); } function testWorkReceiptRegistryRejectsDuplicateReceipt() public { @@ -534,12 +863,72 @@ contract LiveV0PackageTest { _assertTrue(report.exists); } + function testVerifierReportRegistryAcceptsAllV0StatusesAsAdvisoryReports() public { + VerifierReportRegistry registry = new VerifierReportRegistry(); + uint8[5] memory statuses = + [registry.VALID(), registry.INVALID(), registry.UNRESOLVED(), registry.UNSUPPORTED(), registry.REORGED()]; + + registry.setVerifierAuthorization(address(this), true); + + for (uint256 i = 0; i < statuses.length; i++) { + bytes32 reportId = keccak256(abi.encode("verifier.report.status", i)); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.status"), + keccak256(abi.encode("receipt.status", i)), + statuses[i], + keccak256(abi.encode("report.digest", i)), + keccak256(abi.encode("evidence.commitment", i)), + "" + ); + + VerifierReportRegistry.VerifierReport memory report = registry.getVerifierReport(reportId); + _assertTrue(report.status == statuses[i]); + _assertTrue(report.exists); + } + } + + function testVerifierReportRegistryRejectsNonOwnerVerifierAuthorization() public { + VerifierReportRegistry registry = new VerifierReportRegistry(); + VerifierReportRegistryCaller caller = new VerifierReportRegistryCaller(); + + vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.NotOwner.selector, address(caller))); + caller.setVerifierAuthorization(registry, address(this), true); + + vm.expectRevert(VerifierReportRegistry.ZeroVerifier.selector); + registry.setVerifierAuthorization(address(0), true); + } + + function testVerifierReportRegistryBlocksRevokedVerifier() public { + VerifierReportRegistry registry = new VerifierReportRegistry(); + bytes32 reportId = keccak256("verifier.report.revoked"); + uint8 status = registry.VALID(); + + registry.setVerifierAuthorization(address(this), true); + _assertTrue(registry.isAuthorizedVerifier(address(this))); + + registry.setVerifierAuthorization(address(this), false); + _assertTrue(!registry.isAuthorizedVerifier(address(this))); + + vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.VerifierNotAuthorized.selector, address(this))); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.revoked"), + keccak256("receipt.revoked"), + status, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + } + function testVerifierReportRegistryRejectsUnauthorizedInvalidStatusAndDuplicates() public { VerifierReportRegistry registry = new VerifierReportRegistry(); bytes32 reportId = keccak256("verifier.report.beta"); uint8 validStatus = registry.VALID(); uint8 unresolvedStatus = registry.UNRESOLVED(); uint8 reorgedStatus = registry.REORGED(); + uint8 statusAfterReorged = reorgedStatus + 1; vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.VerifierNotAuthorized.selector, address(this))); registry.submitVerifierReport( @@ -554,6 +943,28 @@ contract LiveV0PackageTest { registry.setVerifierAuthorization(address(this), true); + vm.expectRevert(VerifierReportRegistry.ZeroReportId.selector); + registry.submitVerifierReport( + bytes32(0), + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + validStatus, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + + vm.expectRevert(VerifierReportRegistry.ZeroReportTarget.selector); + registry.submitVerifierReport( + reportId, + bytes32(0), + bytes32(0), + validStatus, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.InvalidReportStatus.selector, 0)); registry.submitVerifierReport( reportId, @@ -565,6 +976,39 @@ contract LiveV0PackageTest { "" ); + vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.InvalidReportStatus.selector, statusAfterReorged)); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + statusAfterReorged, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + + vm.expectRevert(VerifierReportRegistry.ZeroReportDigest.selector); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + validStatus, + bytes32(0), + keccak256("evidence.commitment"), + "" + ); + + vm.expectRevert(VerifierReportRegistry.ZeroEvidenceCommitment.selector); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + validStatus, + keccak256("report.digest"), + bytes32(0), + "" + ); + registry.submitVerifierReport( reportId, keccak256("rootfield.beta"), @@ -633,20 +1077,11 @@ contract LiveV0PackageTest { bytes32 rootfieldId = keccak256("rootfield.v4"); bytes32 commitment = keccak256("hook.commitment.v4"); bytes32 parentPulseId = keccak256("parent.pulse"); - IUniswapV4SwapHookLike.PoolKey memory key = IUniswapV4SwapHookLike.PoolKey({ - currency0: address(0x1000), - currency1: address(0x2000), - fee: 3000, - tickSpacing: 60, - hooks: address(adapter) - }); - IUniswapV4SwapHookLike.SwapParams memory params = IUniswapV4SwapHookLike.SwapParams({ - zeroForOne: true, - amountSpecified: -1 ether, - sqrtPriceLimitX96: 42 - }); - bytes memory hookData = - adapter.encodeSwapHookData(rootfieldId, commitment, parentPulseId, "flowmemory://uniswap-v4/canary-after-swap"); + IUniswapV4SwapHookLike.PoolKey memory key = _samplePoolKey(address(adapter)); + IUniswapV4SwapHookLike.SwapParams memory params = _sampleSwapParams(); + bytes memory hookData = adapter.encodeSwapHookData( + rootfieldId, commitment, parentPulseId, "flowmemory://uniswap-v4/canary-after-swap" + ); vm.recordLogs(); (bytes4 selector, int128 hookDelta) = adapter.afterSwap(address(this), key, params, int256(123), hookData); @@ -660,14 +1095,26 @@ contract LiveV0PackageTest { _assertTrue(logs[1].topics[2] == rootfieldId); _assertTrue(logs[1].topics[3] == bytes32(uint256(uint160(address(this))))); _assertSwapPulseData( - logs[1].data, - poolId, - commitment, - parentPulseId, - "flowmemory://uniswap-v4/canary-after-swap" + logs[1].data, poolId, commitment, parentPulseId, "flowmemory://uniswap-v4/canary-after-swap" ); } + function testFlowMemoryHookAdapterUsesDefaultUriForEmptyUniswapV4HookUri() public { + FlowMemoryHookAdapter adapter = new FlowMemoryHookAdapter(); + bytes32 rootfieldId = keccak256("rootfield.v4.default-uri"); + bytes32 commitment = keccak256("hook.commitment.v4.default-uri"); + IUniswapV4SwapHookLike.PoolKey memory key = _samplePoolKey(address(adapter)); + IUniswapV4SwapHookLike.SwapParams memory params = _sampleSwapParams(); + bytes memory hookData = adapter.encodeSwapHookData(rootfieldId, commitment, bytes32(0), ""); + + vm.recordLogs(); + adapter.afterSwap(address(this), key, params, int256(123), hookData); + LiveV0Vm.Log[] memory logs = vm.getRecordedLogs(); + + bytes32 poolId = keccak256(abi.encode(key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks)); + _assertSwapPulseData(logs[1].data, poolId, commitment, bytes32(0), "flowmemory://uniswap-v4/after-swap"); + } + function testFlowMemoryHookAdapterRejectsZeroCommitment() public { FlowMemoryHookAdapter adapter = new FlowMemoryHookAdapter(); @@ -692,27 +1139,48 @@ contract LiveV0PackageTest { function testFlowMemoryHookAdapterRejectsEmptyUniswapV4HookData() public { FlowMemoryHookAdapter adapter = new FlowMemoryHookAdapter(); - IUniswapV4SwapHookLike.PoolKey memory key = IUniswapV4SwapHookLike.PoolKey({ - currency0: address(0x1000), - currency1: address(0x2000), - fee: 3000, - tickSpacing: 60, - hooks: address(adapter) - }); - IUniswapV4SwapHookLike.SwapParams memory params = IUniswapV4SwapHookLike.SwapParams({ - zeroForOne: true, - amountSpecified: -1 ether, - sqrtPriceLimitX96: 42 - }); + IUniswapV4SwapHookLike.PoolKey memory key = _samplePoolKey(address(adapter)); + IUniswapV4SwapHookLike.SwapParams memory params = _sampleSwapParams(); vm.expectRevert(FlowMemoryHookAdapter.EmptyHookData.selector); adapter.afterSwap(address(this), key, params, int256(0), ""); } + function testFlowMemoryHookAdapterRejectsInvalidUniswapV4HookInputs() public { + FlowMemoryHookAdapter adapter = new FlowMemoryHookAdapter(); + IUniswapV4SwapHookLike.PoolKey memory key = _samplePoolKey(address(adapter)); + IUniswapV4SwapHookLike.SwapParams memory params = _sampleSwapParams(); + bytes memory validHookData = + adapter.encodeSwapHookData(keccak256("rootfield.v4"), keccak256("commitment.v4"), bytes32(0), ""); + + vm.expectRevert(FlowMemoryHookAdapter.ZeroSender.selector); + adapter.afterSwap(address(0), key, params, int256(0), validHookData); + + bytes memory zeroRootfieldData = + adapter.encodeSwapHookData(bytes32(0), keccak256("commitment.v4"), bytes32(0), ""); + vm.expectRevert(FlowMemoryHookAdapter.ZeroRootfieldId.selector); + adapter.afterSwap(address(this), key, params, int256(0), zeroRootfieldData); + + bytes memory zeroCommitmentData = + adapter.encodeSwapHookData(keccak256("rootfield.v4"), bytes32(0), bytes32(0), ""); + vm.expectRevert(FlowMemoryHookAdapter.ZeroCommitment.selector); + adapter.afterSwap(address(this), key, params, int256(0), zeroCommitmentData); + } + function _assertTrue(bool condition) private pure { if (!condition) revert AssertionFailed(); } + function _samplePoolKey(address hooks) private pure returns (IUniswapV4SwapHookLike.PoolKey memory) { + return IUniswapV4SwapHookLike.PoolKey({ + currency0: address(0x1000), currency1: address(0x2000), fee: 3000, tickSpacing: 60, hooks: hooks + }); + } + + function _sampleSwapParams() private pure returns (IUniswapV4SwapHookLike.SwapParams memory) { + return IUniswapV4SwapHookLike.SwapParams({zeroForOne: true, amountSpecified: -1 ether, sqrtPriceLimitX96: 42}); + } + function _assertSwapPulseData( bytes memory data, bytes32 expectedSubject, diff --git a/tests/README.md b/tests/README.md index e15061db..01bc79b7 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,3 +16,12 @@ Run a specific suite when iterating: forge test --match-contract RootfieldRegistryTest forge test --match-contract LiveV0PackageTest ``` + +Before handing contract changes to review, run the local hardening wrapper and whitespace diff check: + +```powershell +npm run contracts:hardening +git diff --check +``` + +`npm run contracts:hardening` runs `forge build`, `forge test`, and Slither when it is installed. Use `npm run contracts:hardening:slither` when an audit or review explicitly requires Slither. diff --git a/tests/RootfieldRegistry.t.sol b/tests/RootfieldRegistry.t.sol index f3bbb051..d5adcc3b 100644 --- a/tests/RootfieldRegistry.t.sol +++ b/tests/RootfieldRegistry.t.sol @@ -24,6 +24,10 @@ contract RootfieldRegistryCaller { function deactivateRootfield(RootfieldRegistry registry, bytes32 rootfieldId) external { registry.deactivateRootfield(rootfieldId, bytes32(0), "rootfield://deactivate"); } + + function transferRootfieldOwnership(RootfieldRegistry registry, bytes32 rootfieldId, address newOwner) external { + registry.transferRootfieldOwnership(rootfieldId, newOwner, "rootfield://transfer"); + } } contract RootfieldRegistryTest { @@ -176,6 +180,11 @@ contract RootfieldRegistryTest { registry.registerRootfield(bytes32(0), keccak256("schema.v0"), keccak256("metadata"), ""); } + function testCannotRegisterZeroSchemaHash() public { + vm.expectRevert(RootfieldRegistry.ZeroSchemaHash.selector); + registry.registerRootfield(keccak256("rootfield.zero-schema"), bytes32(0), keccak256("metadata"), ""); + } + function testCannotRegisterDuplicateRootfieldId() public { bytes32 rootfieldId = keccak256("rootfield.gamma"); registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); @@ -206,6 +215,14 @@ contract RootfieldRegistryTest { registry.submitRoot(rootfieldId, bytes32(0), keccak256("artifact"), bytes32(0), ""); } + function testCannotSubmitZeroArtifactCommitment() public { + bytes32 rootfieldId = keccak256("rootfield.zero-artifact"); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + vm.expectRevert(RootfieldRegistry.ZeroArtifactCommitment.selector); + registry.submitRoot(rootfieldId, keccak256("root"), bytes32(0), bytes32(0), ""); + } + function testCannotSubmitUnregisteredRootfield() public { bytes32 rootfieldId = keccak256("rootfield.missing"); @@ -213,6 +230,13 @@ contract RootfieldRegistryTest { registry.submitRoot(rootfieldId, keccak256("root"), keccak256("artifact"), bytes32(0), ""); } + function testCannotDeactivateUnregisteredRootfield() public { + bytes32 rootfieldId = keccak256("rootfield.deactivate.missing"); + + vm.expectRevert(abi.encodeWithSelector(RootfieldRegistry.RootfieldNotRegistered.selector, rootfieldId)); + registry.deactivateRootfield(rootfieldId, bytes32(0), ""); + } + function testOnlyRootfieldOwnerCanSubmitRoot() public { bytes32 rootfieldId = keccak256("rootfield.epsilon"); registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); @@ -264,6 +288,16 @@ contract RootfieldRegistryTest { registry.submitRoot(rootfieldId, keccak256("root"), keccak256("artifact"), pulseId, ""); } + function testCannotDeactivateInactiveRootfield() public { + bytes32 rootfieldId = keccak256("rootfield.deactivate.inactive"); + bytes32 registrationPulseId = + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + registry.deactivateRootfield(rootfieldId, registrationPulseId, "rootfield://deactivate"); + + vm.expectRevert(abi.encodeWithSelector(RootfieldRegistry.RootfieldInactive.selector, rootfieldId)); + registry.deactivateRootfield(rootfieldId, registrationPulseId, "rootfield://deactivate-again"); + } + function testOnlyRootfieldOwnerCanDeactivateRootfield() public { bytes32 rootfieldId = keccak256("rootfield.deactivate.owner"); registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); @@ -298,6 +332,18 @@ contract RootfieldRegistryTest { _assertTrue(rootfield.pulseCount == 3); } + function testOnlyRootfieldOwnerCanTransferRootfieldOwnership() public { + bytes32 rootfieldId = keccak256("rootfield.transfer.owner"); + RootfieldRegistryCaller caller = new RootfieldRegistryCaller(); + RootfieldRegistryCaller newOwner = new RootfieldRegistryCaller(); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + vm.expectRevert( + abi.encodeWithSelector(RootfieldRegistry.NotRootfieldOwner.selector, rootfieldId, address(caller)) + ); + caller.transferRootfieldOwnership(registry, rootfieldId, address(newOwner)); + } + function testTransferRootfieldOwnershipEmitsStatusPulseAndOwnershipEvent() public { bytes32 rootfieldId = keccak256("rootfield.transfer.events"); RootfieldRegistryCaller newOwner = new RootfieldRegistryCaller(); @@ -350,6 +396,14 @@ contract RootfieldRegistryTest { registry.transferRootfieldOwnership(rootfieldId, address(0), ""); } + function testCannotTransferUnregisteredRootfieldOwnership() public { + bytes32 rootfieldId = keccak256("rootfield.transfer.missing"); + RootfieldRegistryCaller newOwner = new RootfieldRegistryCaller(); + + vm.expectRevert(abi.encodeWithSelector(RootfieldRegistry.RootfieldNotRegistered.selector, rootfieldId)); + registry.transferRootfieldOwnership(rootfieldId, address(newOwner), ""); + } + function testCannotTransferInactiveRootfieldOwnership() public { bytes32 rootfieldId = keccak256("rootfield.transfer.inactive"); bytes32 registrationPulseId =