From f4cbdf0def42081bedc103e5694f221b7a69f37c Mon Sep 17 00:00:00 2001 From: FlowMemory Bootstrap Agent Date: Wed, 13 May 2026 00:42:44 -0500 Subject: [PATCH] Build contracts v0 foundation --- contracts/ArtifactRegistry.sol | 78 +++ contracts/CursorRegistry.sol | 80 +++ contracts/FLOWPULSE_SCHEMA.md | 4 +- contracts/FlowMemoryHookAdapter.sol | 31 + contracts/FlowPulse.sol | 2 +- contracts/ReceiptVerifier.sol | 56 ++ contracts/RootfieldRegistry.sol | 65 +- contracts/VerifierRegistry.sol | 76 +++ contracts/VerifierReportRegistry.sol | 94 +++ contracts/WorkDebtScheduler.sol | 81 +++ contracts/WorkReceiptRegistry.sol | 112 ++++ contracts/WorkerRegistry.sol | 76 +++ contracts/interfaces/IArtifactRegistry.sol | 52 ++ contracts/interfaces/ICursorRegistry.sol | 49 ++ .../interfaces/IFlowMemoryHookAdapter.sol | 17 + contracts/interfaces/IReceiptVerifier.sol | 40 ++ contracts/interfaces/IRootfieldRegistry.sol | 29 + contracts/interfaces/IVerifierRegistry.sol | 45 ++ .../interfaces/IVerifierReportRegistry.sol | 42 ++ contracts/interfaces/IWorkDebtScheduler.sol | 56 ++ contracts/interfaces/IWorkReceiptRegistry.sol | 48 ++ contracts/interfaces/IWorkerRegistry.sol | 39 ++ ...26-05-12-flowpulse-observation-identity.md | 122 ++++ docs/DECISIONS/2026-05-12-flowpulse-v0.md | 78 +++ .../2026-05-12-hook-adapter-v0-boundary.md | 38 ++ .../2026-05-12-rootfield-v0-boundary.md | 93 +++ ...026-05-13-artifact-registry-v0-boundary.md | 47 ++ ...ork-receipt-verifier-report-v0-boundary.md | 72 ++ ...13-worker-verifier-registry-v0-boundary.md | 47 ++ docs/SECURITY_MODEL.md | 23 + foundry.toml | 9 + tests/LiveV0Package.t.sol | 614 ++++++++++++++++++ tests/README.md | 11 +- tests/RootfieldRegistry.t.sol | 227 ++++++- 34 files changed, 2522 insertions(+), 31 deletions(-) create mode 100644 contracts/ArtifactRegistry.sol create mode 100644 contracts/CursorRegistry.sol create mode 100644 contracts/FlowMemoryHookAdapter.sol create mode 100644 contracts/ReceiptVerifier.sol create mode 100644 contracts/VerifierRegistry.sol create mode 100644 contracts/VerifierReportRegistry.sol create mode 100644 contracts/WorkDebtScheduler.sol create mode 100644 contracts/WorkReceiptRegistry.sol create mode 100644 contracts/WorkerRegistry.sol create mode 100644 contracts/interfaces/IArtifactRegistry.sol create mode 100644 contracts/interfaces/ICursorRegistry.sol create mode 100644 contracts/interfaces/IFlowMemoryHookAdapter.sol create mode 100644 contracts/interfaces/IReceiptVerifier.sol create mode 100644 contracts/interfaces/IRootfieldRegistry.sol create mode 100644 contracts/interfaces/IVerifierRegistry.sol create mode 100644 contracts/interfaces/IVerifierReportRegistry.sol create mode 100644 contracts/interfaces/IWorkDebtScheduler.sol create mode 100644 contracts/interfaces/IWorkReceiptRegistry.sol create mode 100644 contracts/interfaces/IWorkerRegistry.sol create mode 100644 docs/DECISIONS/2026-05-12-flowpulse-observation-identity.md create mode 100644 docs/DECISIONS/2026-05-12-flowpulse-v0.md create mode 100644 docs/DECISIONS/2026-05-12-hook-adapter-v0-boundary.md create mode 100644 docs/DECISIONS/2026-05-12-rootfield-v0-boundary.md create mode 100644 docs/DECISIONS/2026-05-13-artifact-registry-v0-boundary.md create mode 100644 docs/DECISIONS/2026-05-13-work-receipt-verifier-report-v0-boundary.md create mode 100644 docs/DECISIONS/2026-05-13-worker-verifier-registry-v0-boundary.md create mode 100644 foundry.toml create mode 100644 tests/LiveV0Package.t.sol diff --git a/contracts/ArtifactRegistry.sol b/contracts/ArtifactRegistry.sol new file mode 100644 index 00000000..13191e5f --- /dev/null +++ b/contracts/ArtifactRegistry.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IArtifactRegistry} from "./interfaces/IArtifactRegistry.sol"; + +/// @title ArtifactRegistry +/// @notice Minimal v0 registry for artifact commitments. +/// @dev URI values are advisory log data only. Raw artifacts remain off-chain. +contract ArtifactRegistry is IArtifactRegistry { + mapping(bytes32 artifactId => Artifact artifact) private _artifacts; + + error ZeroArtifactId(); + error ZeroRootfieldId(); + error ZeroArtifactType(); + error ZeroCommitmentHash(); + error ArtifactAlreadyRegistered(bytes32 artifactId); + error ArtifactNotRegistered(bytes32 artifactId); + error ArtifactNotActive(bytes32 artifactId); + error NotArtifactOwner(bytes32 artifactId, address caller); + error TimestampOverflow(uint256 timestamp); + + function registerArtifact( + bytes32 artifactId, + bytes32 rootfieldId, + bytes32 artifactType, + bytes32 commitmentHash, + bytes32 schemaHash, + bytes32 metadataHash, + string calldata artifactURI + ) external { + if (artifactId == bytes32(0)) revert ZeroArtifactId(); + if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); + if (artifactType == bytes32(0)) revert ZeroArtifactType(); + if (commitmentHash == bytes32(0)) revert ZeroCommitmentHash(); + if (_artifacts[artifactId].exists) revert ArtifactAlreadyRegistered(artifactId); + + uint64 now64 = _blockTimestamp(); + _artifacts[artifactId] = Artifact({ + owner: msg.sender, + submitter: msg.sender, + rootfieldId: rootfieldId, + artifactType: artifactType, + commitmentHash: commitmentHash, + schemaHash: schemaHash, + metadataHash: metadataHash, + status: ArtifactStatus.Active, + registeredAt: now64, + updatedAt: now64, + exists: true + }); + + emit ArtifactRegistered( + artifactId, msg.sender, rootfieldId, artifactType, commitmentHash, schemaHash, metadataHash, artifactURI + ); + } + + function deprecateArtifact(bytes32 artifactId, bytes32 metadataHash, string calldata evidenceURI) external { + Artifact storage artifact = _artifacts[artifactId]; + if (!artifact.exists) revert ArtifactNotRegistered(artifactId); + if (artifact.owner != msg.sender) revert NotArtifactOwner(artifactId, msg.sender); + if (artifact.status != ArtifactStatus.Active) revert ArtifactNotActive(artifactId); + + artifact.metadataHash = metadataHash; + artifact.status = ArtifactStatus.Deprecated; + artifact.updatedAt = _blockTimestamp(); + + emit ArtifactDeprecated(artifactId, msg.sender, metadataHash, evidenceURI); + } + + function getArtifact(bytes32 artifactId) external view returns (Artifact memory) { + return _artifacts[artifactId]; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/CursorRegistry.sol b/contracts/CursorRegistry.sol new file mode 100644 index 00000000..abd03d20 --- /dev/null +++ b/contracts/CursorRegistry.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ICursorRegistry} from "./interfaces/ICursorRegistry.sol"; + +/// @title CursorRegistry +/// @notice Minimal v0 registry for off-chain indexer cursor commitments. +/// @dev This skeleton stores compact commitments only. It does not define +/// canonical indexer identity, receipt identity, or chain reorg policy. +contract CursorRegistry is ICursorRegistry { + mapping(bytes32 cursorId => Cursor cursor) private _cursors; + + error ZeroCursorId(); + error ZeroStreamId(); + error ZeroPositionCommitment(); + error CursorAlreadyRegistered(bytes32 cursorId); + error CursorNotRegistered(bytes32 cursorId); + error NotCursorOwner(bytes32 cursorId, address caller); + error TimestampOverflow(uint256 timestamp); + + function registerCursor( + bytes32 cursorId, + bytes32 streamId, + bytes32 positionCommitment, + bytes32 metadataHash, + string calldata metadataURI + ) external { + if (cursorId == bytes32(0)) revert ZeroCursorId(); + if (streamId == bytes32(0)) revert ZeroStreamId(); + if (positionCommitment == bytes32(0)) revert ZeroPositionCommitment(); + if (_cursors[cursorId].owner != address(0)) revert CursorAlreadyRegistered(cursorId); + + uint64 now64 = _blockTimestamp(); + _cursors[cursorId] = Cursor({ + owner: msg.sender, + streamId: streamId, + positionCommitment: positionCommitment, + metadataHash: metadataHash, + updateCount: 1, + updatedAt: now64, + active: true + }); + + emit CursorRegistered(cursorId, msg.sender, streamId, positionCommitment, metadataHash, metadataURI); + } + + function advanceCursor( + bytes32 cursorId, + bytes32 positionCommitment, + bytes32 metadataHash, + string calldata evidenceURI + ) external { + if (positionCommitment == bytes32(0)) revert ZeroPositionCommitment(); + + Cursor storage cursor = _requireCursorOwner(cursorId); + cursor.positionCommitment = positionCommitment; + cursor.metadataHash = metadataHash; + cursor.updateCount += 1; + cursor.updatedAt = _blockTimestamp(); + + emit CursorAdvanced( + cursorId, msg.sender, cursor.streamId, positionCommitment, metadataHash, cursor.updateCount, evidenceURI + ); + } + + function getCursor(bytes32 cursorId) external view returns (Cursor memory) { + return _cursors[cursorId]; + } + + function _requireCursorOwner(bytes32 cursorId) private view returns (Cursor storage cursor) { + cursor = _cursors[cursorId]; + if (cursor.owner == address(0)) revert CursorNotRegistered(cursorId); + if (cursor.owner != msg.sender) revert NotCursorOwner(cursorId, msg.sender); + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/FLOWPULSE_SCHEMA.md b/contracts/FLOWPULSE_SCHEMA.md index caccc892..0c576121 100644 --- a/contracts/FLOWPULSE_SCHEMA.md +++ b/contracts/FLOWPULSE_SCHEMA.md @@ -24,8 +24,8 @@ event FlowPulse( - `pulseId`: Domain-separated identifier created by the emitting contract. It is not a replacement for receipt metadata. - `rootfieldId`: Namespace for the committed state stream. - `actor`: Account that caused the pulse. -- `pulseType`: Stable numeric type. Initial reserved values are `1` for rootfield registration, `2` for root commitment, and `3` for rootfield status changes. -- `subject`: Type-specific subject. For registration this is the rootfield id. For root commitment this is the committed root. +- `pulseType`: Stable numeric type. Initial reserved values are `1` for rootfield registration, `2` for root commitment, and `3` for rootfield lifecycle/status changes such as deactivation or ownership transfer. +- `subject`: Type-specific subject. For registration and rootfield lifecycle changes this is the rootfield id. For root commitment this is the committed root. - `commitment`: Type-specific hash commitment to off-chain data or metadata. Heavy AI, model, memory, artifact, and media data stays off-chain. - `parentPulseId`: Optional prior pulse reference for chains of work or verification. - `sequence`: Monotonic sequence within the rootfield namespace. diff --git a/contracts/FlowMemoryHookAdapter.sol b/contracts/FlowMemoryHookAdapter.sol new file mode 100644 index 00000000..8da77278 --- /dev/null +++ b/contracts/FlowMemoryHookAdapter.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IFlowMemoryHookAdapter} from "./interfaces/IFlowMemoryHookAdapter.sol"; + +/// @title FlowMemoryHookAdapter +/// @notice Dependency-light scaffold for future Uniswap v4 hook integration. +/// @dev This is not a production hook. It performs no custom accounting, no +/// dynamic fees, no token custody, and no external protocol calls. It cannot +/// know txHash or logIndex during execution; indexers derive receipt metadata. +contract FlowMemoryHookAdapter is IFlowMemoryHookAdapter { + bytes4 public constant AFTER_SWAP_SELECTOR = bytes4(keccak256("afterSwap(address,bytes32,bytes32,bytes32,bytes)")); + + error ZeroSender(); + error ZeroPoolId(); + error ZeroRootfieldId(); + error ZeroCommitment(); + + function afterSwap(address sender, bytes32 poolId, bytes32 rootfieldId, bytes32 commitment, bytes calldata hookData) + external + returns (bytes4 selector) + { + if (sender == address(0)) revert ZeroSender(); + if (poolId == bytes32(0)) revert ZeroPoolId(); + if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); + if (commitment == bytes32(0)) revert ZeroCommitment(); + + emit AfterSwapObserved(msg.sender, sender, poolId, rootfieldId, commitment, keccak256(hookData)); + return AFTER_SWAP_SELECTOR; + } +} diff --git a/contracts/FlowPulse.sol b/contracts/FlowPulse.sol index 0f1e75dd..18d4b998 100644 --- a/contracts/FlowPulse.sol +++ b/contracts/FlowPulse.sol @@ -16,7 +16,7 @@ interface IFlowPulse { /// @param parentPulseId Optional prior pulse being extended or referenced. /// @param sequence Monotonic sequence within the rootfield namespace. /// @param occurredAt Block timestamp observed by the emitting contract. - /// @param uri Optional short off-chain pointer; not a payload storage field. + /// @param uri Arbitrary advisory string emitted as on-chain log data. event FlowPulse( bytes32 indexed pulseId, bytes32 indexed rootfieldId, diff --git a/contracts/ReceiptVerifier.sol b/contracts/ReceiptVerifier.sol new file mode 100644 index 00000000..4ec93ff5 --- /dev/null +++ b/contracts/ReceiptVerifier.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IReceiptVerifier} from "./interfaces/IReceiptVerifier.sol"; + +/// @title ReceiptVerifier +/// @notice Minimal v0 receipt-report commitment registry. +/// @dev This skeleton does not verify chain receipts cryptographically and does +/// not know txHash or logIndex during contract execution. It stores compact +/// commitments that off-chain verifiers can reconcile against receipts. +contract ReceiptVerifier is IReceiptVerifier { + mapping(bytes32 reportId => ReceiptReport report) private _reports; + + error ZeroReportId(); + error ZeroObservationId(); + error ZeroReceiptCommitment(); + error ReceiptReportAlreadySubmitted(bytes32 reportId); + error TimestampOverflow(uint256 timestamp); + + function submitReceiptReport( + bytes32 reportId, + bytes32 observationId, + bytes32 rootfieldId, + bytes32 receiptCommitment, + bytes32 reportHash, + string calldata evidenceURI + ) external { + if (reportId == bytes32(0)) revert ZeroReportId(); + if (observationId == bytes32(0)) revert ZeroObservationId(); + if (receiptCommitment == bytes32(0)) revert ZeroReceiptCommitment(); + if (_reports[reportId].status != ReceiptStatus.Unknown) revert ReceiptReportAlreadySubmitted(reportId); + + _reports[reportId] = ReceiptReport({ + reporter: msg.sender, + observationId: observationId, + rootfieldId: rootfieldId, + receiptCommitment: receiptCommitment, + reportHash: reportHash, + status: ReceiptStatus.Submitted, + submittedAt: _blockTimestamp() + }); + + emit ReceiptReportSubmitted( + reportId, msg.sender, observationId, rootfieldId, receiptCommitment, reportHash, evidenceURI + ); + } + + function getReceiptReport(bytes32 reportId) external view returns (ReceiptReport memory) { + return _reports[reportId]; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/RootfieldRegistry.sol b/contracts/RootfieldRegistry.sol index 0d81f035..56edf206 100644 --- a/contracts/RootfieldRegistry.sol +++ b/contracts/RootfieldRegistry.sol @@ -26,8 +26,16 @@ contract RootfieldRegistry is IFlowPulse { error RootfieldNotRegistered(bytes32 rootfieldId); error RootfieldInactive(bytes32 rootfieldId); error NotRootfieldOwner(bytes32 rootfieldId, address caller); + error ZeroRootfieldOwner(); error TimestampOverflow(uint256 timestamp); + event RootfieldDeactivated( + bytes32 indexed rootfieldId, address indexed owner, bytes32 indexed parentPulseId, string reasonURI + ); + event RootfieldOwnershipTransferred( + bytes32 indexed rootfieldId, address indexed previousOwner, address indexed newOwner, string evidenceURI + ); + function registerRootfield( bytes32 rootfieldId, bytes32 schemaHash, @@ -91,6 +99,59 @@ contract RootfieldRegistry is IFlowPulse { }); } + function deactivateRootfield(bytes32 rootfieldId, bytes32 parentPulseId, string calldata reasonURI) + external + returns (bytes32 pulseId) + { + Rootfield storage rootfield = _requireRootfieldOwner(rootfieldId); + if (!rootfield.active) { + revert RootfieldInactive(rootfieldId); + } + + rootfield.active = false; + + pulseId = _emitFlowPulse({ + rootfieldId: rootfieldId, + actor: msg.sender, + pulseType: FlowPulseTypes.ROOTFIELD_STATUS_CHANGED, + subject: rootfieldId, + commitment: keccak256(abi.encode(rootfieldId, false)), + parentPulseId: parentPulseId, + uri: reasonURI + }); + + emit RootfieldDeactivated(rootfieldId, msg.sender, parentPulseId, reasonURI); + } + + function transferRootfieldOwnership(bytes32 rootfieldId, address newOwner, string calldata evidenceURI) + external + returns (bytes32 pulseId) + { + if (newOwner == address(0)) { + revert ZeroRootfieldOwner(); + } + + Rootfield storage rootfield = _requireRootfieldOwner(rootfieldId); + if (!rootfield.active) { + revert RootfieldInactive(rootfieldId); + } + + address previousOwner = rootfield.owner; + rootfield.owner = newOwner; + + pulseId = _emitFlowPulse({ + rootfieldId: rootfieldId, + actor: previousOwner, + pulseType: FlowPulseTypes.ROOTFIELD_STATUS_CHANGED, + subject: rootfieldId, + commitment: keccak256(abi.encode(previousOwner, newOwner)), + parentPulseId: bytes32(0), + uri: evidenceURI + }); + + emit RootfieldOwnershipTransferred(rootfieldId, previousOwner, newOwner, evidenceURI); + } + function getRootfield(bytes32 rootfieldId) external view returns (Rootfield memory) { return _rootfields[rootfieldId]; } @@ -136,7 +197,9 @@ contract RootfieldRegistry is IFlowPulse { ) ); - emit FlowPulse(pulseId, rootfieldId, actor, pulseType, subject, commitment, parentPulseId, sequence, occurredAt, uri); + emit FlowPulse( + pulseId, rootfieldId, actor, pulseType, subject, commitment, parentPulseId, sequence, occurredAt, uri + ); } function _nextSequence(bytes32 rootfieldId) private returns (uint64 sequence) { diff --git a/contracts/VerifierRegistry.sol b/contracts/VerifierRegistry.sol new file mode 100644 index 00000000..94864881 --- /dev/null +++ b/contracts/VerifierRegistry.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IVerifierRegistry} from "./interfaces/IVerifierRegistry.sol"; + +/// @title VerifierRegistry +/// @notice Minimal v0 self-registry for verifier identities and metadata commitments. +/// @dev Registration does not prove verifier correctness or authorize any rewards. +contract VerifierRegistry is IVerifierRegistry { + mapping(address verifier => Verifier record) private _verifiers; + + error ZeroVerifier(); + error ZeroOperatorId(); + error ZeroVerifierRole(); + error VerifierAlreadyRegistered(address verifier); + error VerifierNotRegistered(address verifier); + error VerifierNotActive(address verifier); + error TimestampOverflow(uint256 timestamp); + + function registerVerifier(bytes32 operatorId, bytes32 role, bytes32 metadataHash, string calldata metadataURI) + external + { + if (msg.sender == address(0)) revert ZeroVerifier(); + if (operatorId == bytes32(0)) revert ZeroOperatorId(); + if (role == bytes32(0)) revert ZeroVerifierRole(); + if (_verifiers[msg.sender].status != VerifierStatus.Unknown) revert VerifierAlreadyRegistered(msg.sender); + + uint64 now64 = _blockTimestamp(); + _verifiers[msg.sender] = Verifier({ + operatorId: operatorId, + role: role, + metadataHash: metadataHash, + status: VerifierStatus.Active, + registeredAt: now64, + updatedAt: now64, + updateCount: 1, + active: true + }); + + emit VerifierRegistered(msg.sender, operatorId, role, metadataHash, metadataURI); + } + + function updateVerifierMetadata(bytes32 metadataHash, string calldata metadataURI) external { + Verifier storage verifier = _verifiers[msg.sender]; + if (verifier.status == VerifierStatus.Unknown) revert VerifierNotRegistered(msg.sender); + if (verifier.status != VerifierStatus.Active) revert VerifierNotActive(msg.sender); + + verifier.metadataHash = metadataHash; + verifier.updatedAt = _blockTimestamp(); + verifier.updateCount += 1; + + emit VerifierMetadataUpdated(msg.sender, verifier.operatorId, metadataHash, verifier.updateCount, metadataURI); + } + + function deactivateVerifier(bytes32 reasonHash, string calldata evidenceURI) external { + Verifier storage verifier = _verifiers[msg.sender]; + if (verifier.status == VerifierStatus.Unknown) revert VerifierNotRegistered(msg.sender); + if (verifier.status != VerifierStatus.Active) revert VerifierNotActive(msg.sender); + + verifier.status = VerifierStatus.Inactive; + verifier.active = false; + verifier.updatedAt = _blockTimestamp(); + verifier.updateCount += 1; + + emit VerifierDeactivated(msg.sender, verifier.operatorId, reasonHash, evidenceURI); + } + + function getVerifier(address verifier) external view returns (Verifier memory) { + return _verifiers[verifier]; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/VerifierReportRegistry.sol b/contracts/VerifierReportRegistry.sol new file mode 100644 index 00000000..e302cc04 --- /dev/null +++ b/contracts/VerifierReportRegistry.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IVerifierReportRegistry} from "./interfaces/IVerifierReportRegistry.sol"; + +/// @title VerifierReportRegistry +/// @notice Minimal v0 registry for verifier report commitments. +/// @dev Uses an owner-controlled verifier allowlist. It does not implement +/// staking, slashing, rewards, or on-chain receipt verification. +contract VerifierReportRegistry is IVerifierReportRegistry { + uint8 public constant VALID = 1; + uint8 public constant INVALID = 2; + uint8 public constant UNRESOLVED = 3; + uint8 public constant UNSUPPORTED = 4; + uint8 public constant REORGED = 5; + + address public immutable owner; + + mapping(address verifier => bool authorized) private _authorizedVerifiers; + mapping(bytes32 reportId => VerifierReport report) private _reports; + + error NotOwner(address caller); + error ZeroVerifier(); + error VerifierNotAuthorized(address verifier); + error ZeroReportId(); + error ZeroReportTarget(); + error InvalidReportStatus(uint8 status); + error ZeroReportDigest(); + error ZeroEvidenceCommitment(); + error VerifierReportAlreadySubmitted(bytes32 reportId); + error TimestampOverflow(uint256 timestamp); + + constructor() { + owner = msg.sender; + } + + function setVerifierAuthorization(address verifier, bool authorized) external { + if (msg.sender != owner) revert NotOwner(msg.sender); + if (verifier == address(0)) revert ZeroVerifier(); + + _authorizedVerifiers[verifier] = authorized; + emit VerifierAuthorizationSet(verifier, authorized); + } + + function submitVerifierReport( + bytes32 reportId, + bytes32 rootfieldId, + bytes32 receiptId, + uint8 status, + bytes32 reportDigest, + bytes32 evidenceCommitment, + string calldata evidenceURI + ) external { + if (!_authorizedVerifiers[msg.sender]) revert VerifierNotAuthorized(msg.sender); + if (reportId == bytes32(0)) revert ZeroReportId(); + if (rootfieldId == bytes32(0) && receiptId == bytes32(0)) revert ZeroReportTarget(); + if (!_isValidStatus(status)) revert InvalidReportStatus(status); + if (reportDigest == bytes32(0)) revert ZeroReportDigest(); + if (evidenceCommitment == bytes32(0)) revert ZeroEvidenceCommitment(); + if (_reports[reportId].exists) revert VerifierReportAlreadySubmitted(reportId); + + _reports[reportId] = VerifierReport({ + verifier: msg.sender, + rootfieldId: rootfieldId, + receiptId: receiptId, + status: status, + reportDigest: reportDigest, + evidenceCommitment: evidenceCommitment, + submittedAt: _blockTimestamp(), + exists: true + }); + + emit VerifierReportSubmitted( + reportId, msg.sender, receiptId, rootfieldId, status, reportDigest, evidenceCommitment, evidenceURI + ); + } + + function isAuthorizedVerifier(address verifier) external view returns (bool) { + return _authorizedVerifiers[verifier]; + } + + function getVerifierReport(bytes32 reportId) external view returns (VerifierReport memory) { + return _reports[reportId]; + } + + function _isValidStatus(uint8 status) private pure returns (bool) { + return status >= VALID && status <= REORGED; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/WorkDebtScheduler.sol b/contracts/WorkDebtScheduler.sol new file mode 100644 index 00000000..624fb2f8 --- /dev/null +++ b/contracts/WorkDebtScheduler.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IWorkDebtScheduler} from "./interfaces/IWorkDebtScheduler.sol"; + +/// @title WorkDebtScheduler +/// @notice Minimal v0 scheduler for work commitment state. +/// @dev No token debt, rewards, slashing, dynamic fees, or external calls. +contract WorkDebtScheduler is IWorkDebtScheduler { + mapping(bytes32 workId => WorkItem item) private _workItems; + + error ZeroWorkId(); + error ZeroWorker(); + error ZeroRootfieldId(); + error ZeroWorkCommitment(); + error ZeroCompletionCommitment(); + error WorkAlreadyScheduled(bytes32 workId); + error WorkNotScheduled(bytes32 workId); + error WorkNotScheduledStatus(bytes32 workId); + error NotWorkParticipant(bytes32 workId, address caller); + error TimestampOverflow(uint256 timestamp); + + function scheduleWork( + bytes32 workId, + address worker, + bytes32 rootfieldId, + bytes32 workCommitment, + bytes32 metadataHash, + string calldata workURI + ) external { + if (workId == bytes32(0)) revert ZeroWorkId(); + if (worker == address(0)) revert ZeroWorker(); + if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); + if (workCommitment == bytes32(0)) revert ZeroWorkCommitment(); + if (_workItems[workId].status != WorkStatus.Unknown) revert WorkAlreadyScheduled(workId); + + uint64 now64 = _blockTimestamp(); + _workItems[workId] = WorkItem({ + scheduler: msg.sender, + worker: worker, + rootfieldId: rootfieldId, + workCommitment: workCommitment, + metadataHash: metadataHash, + status: WorkStatus.Scheduled, + scheduledAt: now64, + updatedAt: now64 + }); + + emit WorkScheduled(workId, msg.sender, worker, rootfieldId, workCommitment, metadataHash, workURI); + } + + function markWorkComplete( + bytes32 workId, + bytes32 completionCommitment, + bytes32 metadataHash, + string calldata evidenceURI + ) external { + if (completionCommitment == bytes32(0)) revert ZeroCompletionCommitment(); + + WorkItem storage item = _workItems[workId]; + if (item.status == WorkStatus.Unknown) revert WorkNotScheduled(workId); + if (item.status != WorkStatus.Scheduled) revert WorkNotScheduledStatus(workId); + if (msg.sender != item.scheduler && msg.sender != item.worker) revert NotWorkParticipant(workId, msg.sender); + + item.workCommitment = completionCommitment; + item.metadataHash = metadataHash; + item.status = WorkStatus.Completed; + item.updatedAt = _blockTimestamp(); + + emit WorkCompleted(workId, msg.sender, completionCommitment, metadataHash, evidenceURI); + } + + function getWorkItem(bytes32 workId) external view returns (WorkItem memory) { + return _workItems[workId]; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/WorkReceiptRegistry.sol b/contracts/WorkReceiptRegistry.sol new file mode 100644 index 00000000..9f371a21 --- /dev/null +++ b/contracts/WorkReceiptRegistry.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IWorkReceiptRegistry} from "./interfaces/IWorkReceiptRegistry.sol"; + +/// @title WorkReceiptRegistry +/// @notice Minimal v0 registry for compact work receipt commitments. +/// @dev Uses an owner-controlled worker allowlist. It does not implement +/// staking, rewards, slashing, scheduling, or external verifier calls. +contract WorkReceiptRegistry is IWorkReceiptRegistry { + uint8 public constant MEMORY_REFRESH = 1; + uint8 public constant FAILURE_DISCOVERY = 2; + uint8 public constant FAILURE_REPAIR = 3; + uint8 public constant MANIFOLD_DISCOVERY = 4; + uint8 public constant STEERING_VALIDATION = 5; + uint8 public constant CHECKPOINT_STORAGE = 6; + uint8 public constant GPU_TRAINING = 7; + uint8 public constant EVAL_COUNTEREXAMPLE = 8; + + address public immutable owner; + + mapping(address worker => bool authorized) private _authorizedWorkers; + mapping(bytes32 receiptId => WorkReceipt receipt) private _receipts; + + error NotOwner(address caller); + error ZeroWorker(); + error WorkerNotAuthorized(address worker); + error ZeroReceiptId(); + error ZeroRootfieldId(); + error InvalidWorkLane(uint8 lane); + error ZeroInputRoot(); + error ZeroOutputRoot(); + error ZeroArtifactCommitment(); + error WorkReceiptAlreadySubmitted(bytes32 receiptId); + error TimestampOverflow(uint256 timestamp); + + constructor() { + owner = msg.sender; + } + + function setWorkerAuthorization(address worker, bool authorized) external { + if (msg.sender != owner) revert NotOwner(msg.sender); + if (worker == address(0)) revert ZeroWorker(); + + _authorizedWorkers[worker] = authorized; + emit WorkerAuthorizationSet(worker, authorized); + } + + function submitWorkReceipt( + bytes32 receiptId, + bytes32 rootfieldId, + uint8 lane, + bytes32 subject, + bytes32 inputRoot, + bytes32 outputRoot, + bytes32 artifactCommitment, + bytes32 parentReceiptId, + string calldata evidenceURI + ) external { + if (!_authorizedWorkers[msg.sender]) revert WorkerNotAuthorized(msg.sender); + if (receiptId == bytes32(0)) revert ZeroReceiptId(); + if (rootfieldId == bytes32(0)) revert ZeroRootfieldId(); + if (!_isValidLane(lane)) revert InvalidWorkLane(lane); + if (inputRoot == bytes32(0)) revert ZeroInputRoot(); + if (outputRoot == bytes32(0)) revert ZeroOutputRoot(); + if (artifactCommitment == bytes32(0)) revert ZeroArtifactCommitment(); + if (_receipts[receiptId].exists) revert WorkReceiptAlreadySubmitted(receiptId); + + _receipts[receiptId] = WorkReceipt({ + worker: msg.sender, + rootfieldId: rootfieldId, + lane: lane, + subject: subject, + inputRoot: inputRoot, + outputRoot: outputRoot, + artifactCommitment: artifactCommitment, + parentReceiptId: parentReceiptId, + submittedAt: _blockTimestamp(), + exists: true + }); + + emit WorkReceiptSubmitted( + receiptId, + msg.sender, + rootfieldId, + lane, + subject, + inputRoot, + outputRoot, + artifactCommitment, + parentReceiptId, + evidenceURI + ); + } + + function isAuthorizedWorker(address worker) external view returns (bool) { + return _authorizedWorkers[worker]; + } + + function getWorkReceipt(bytes32 receiptId) external view returns (WorkReceipt memory) { + return _receipts[receiptId]; + } + + function _isValidLane(uint8 lane) private pure returns (bool) { + return lane >= MEMORY_REFRESH && lane <= EVAL_COUNTEREXAMPLE; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/WorkerRegistry.sol b/contracts/WorkerRegistry.sol new file mode 100644 index 00000000..8c6359ad --- /dev/null +++ b/contracts/WorkerRegistry.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {IWorkerRegistry} from "./interfaces/IWorkerRegistry.sol"; + +/// @title WorkerRegistry +/// @notice Minimal v0 self-registry for worker identities and metadata commitments. +/// @dev No staking, rewards, slashing, custody, or token mechanics are implemented. +contract WorkerRegistry is IWorkerRegistry { + mapping(address worker => Worker record) private _workers; + + error ZeroWorker(); + error ZeroOperatorId(); + error ZeroWorkerRole(); + error WorkerAlreadyRegistered(address worker); + error WorkerNotRegistered(address worker); + error WorkerNotActive(address worker); + error TimestampOverflow(uint256 timestamp); + + function registerWorker(bytes32 operatorId, bytes32 role, bytes32 metadataHash, string calldata metadataURI) + external + { + if (msg.sender == address(0)) revert ZeroWorker(); + if (operatorId == bytes32(0)) revert ZeroOperatorId(); + if (role == bytes32(0)) revert ZeroWorkerRole(); + if (_workers[msg.sender].status != WorkerStatus.Unknown) revert WorkerAlreadyRegistered(msg.sender); + + uint64 now64 = _blockTimestamp(); + _workers[msg.sender] = Worker({ + operatorId: operatorId, + role: role, + metadataHash: metadataHash, + status: WorkerStatus.Active, + registeredAt: now64, + updatedAt: now64, + updateCount: 1, + active: true + }); + + emit WorkerRegistered(msg.sender, operatorId, role, metadataHash, metadataURI); + } + + function updateWorkerMetadata(bytes32 metadataHash, string calldata metadataURI) external { + Worker storage worker = _workers[msg.sender]; + if (worker.status == WorkerStatus.Unknown) revert WorkerNotRegistered(msg.sender); + if (worker.status != WorkerStatus.Active) revert WorkerNotActive(msg.sender); + + worker.metadataHash = metadataHash; + worker.updatedAt = _blockTimestamp(); + worker.updateCount += 1; + + emit WorkerMetadataUpdated(msg.sender, worker.operatorId, metadataHash, worker.updateCount, metadataURI); + } + + function deactivateWorker(bytes32 reasonHash, string calldata evidenceURI) external { + Worker storage worker = _workers[msg.sender]; + if (worker.status == WorkerStatus.Unknown) revert WorkerNotRegistered(msg.sender); + if (worker.status != WorkerStatus.Active) revert WorkerNotActive(msg.sender); + + worker.status = WorkerStatus.Inactive; + worker.active = false; + worker.updatedAt = _blockTimestamp(); + worker.updateCount += 1; + + emit WorkerDeactivated(msg.sender, worker.operatorId, reasonHash, evidenceURI); + } + + function getWorker(address worker) external view returns (Worker memory) { + return _workers[worker]; + } + + function _blockTimestamp() private view returns (uint64) { + if (block.timestamp > type(uint64).max) revert TimestampOverflow(block.timestamp); + return uint64(block.timestamp); + } +} diff --git a/contracts/interfaces/IArtifactRegistry.sol b/contracts/interfaces/IArtifactRegistry.sol new file mode 100644 index 00000000..b9259a30 --- /dev/null +++ b/contracts/interfaces/IArtifactRegistry.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IArtifactRegistry { + enum ArtifactStatus { + Unknown, + Active, + Deprecated + } + + struct Artifact { + address owner; + address submitter; + bytes32 rootfieldId; + bytes32 artifactType; + bytes32 commitmentHash; + bytes32 schemaHash; + bytes32 metadataHash; + ArtifactStatus status; + uint64 registeredAt; + uint64 updatedAt; + bool exists; + } + + event ArtifactRegistered( + bytes32 indexed artifactId, + address indexed owner, + bytes32 indexed rootfieldId, + bytes32 artifactType, + bytes32 commitmentHash, + bytes32 schemaHash, + bytes32 metadataHash, + string artifactURI + ); + event ArtifactDeprecated( + bytes32 indexed artifactId, address indexed owner, bytes32 metadataHash, string evidenceURI + ); + + function registerArtifact( + bytes32 artifactId, + bytes32 rootfieldId, + bytes32 artifactType, + bytes32 commitmentHash, + bytes32 schemaHash, + bytes32 metadataHash, + string calldata artifactURI + ) external; + + function deprecateArtifact(bytes32 artifactId, bytes32 metadataHash, string calldata evidenceURI) external; + + function getArtifact(bytes32 artifactId) external view returns (Artifact memory); +} diff --git a/contracts/interfaces/ICursorRegistry.sol b/contracts/interfaces/ICursorRegistry.sol new file mode 100644 index 00000000..6cc37a79 --- /dev/null +++ b/contracts/interfaces/ICursorRegistry.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface ICursorRegistry { + struct Cursor { + address owner; + bytes32 streamId; + bytes32 positionCommitment; + bytes32 metadataHash; + uint64 updateCount; + uint64 updatedAt; + bool active; + } + + event CursorRegistered( + bytes32 indexed cursorId, + address indexed owner, + bytes32 indexed streamId, + bytes32 positionCommitment, + bytes32 metadataHash, + string metadataURI + ); + event CursorAdvanced( + bytes32 indexed cursorId, + address indexed owner, + bytes32 indexed streamId, + bytes32 positionCommitment, + bytes32 metadataHash, + uint64 updateCount, + string evidenceURI + ); + + function registerCursor( + bytes32 cursorId, + bytes32 streamId, + bytes32 positionCommitment, + bytes32 metadataHash, + string calldata metadataURI + ) external; + + function advanceCursor( + bytes32 cursorId, + bytes32 positionCommitment, + bytes32 metadataHash, + string calldata evidenceURI + ) external; + + function getCursor(bytes32 cursorId) external view returns (Cursor memory); +} diff --git a/contracts/interfaces/IFlowMemoryHookAdapter.sol b/contracts/interfaces/IFlowMemoryHookAdapter.sol new file mode 100644 index 00000000..bef298a5 --- /dev/null +++ b/contracts/interfaces/IFlowMemoryHookAdapter.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IFlowMemoryHookAdapter { + event AfterSwapObserved( + address indexed caller, + address indexed sender, + bytes32 indexed poolId, + bytes32 rootfieldId, + bytes32 commitment, + bytes32 hookDataHash + ); + + function afterSwap(address sender, bytes32 poolId, bytes32 rootfieldId, bytes32 commitment, bytes calldata hookData) + external + returns (bytes4 selector); +} diff --git a/contracts/interfaces/IReceiptVerifier.sol b/contracts/interfaces/IReceiptVerifier.sol new file mode 100644 index 00000000..17472f64 --- /dev/null +++ b/contracts/interfaces/IReceiptVerifier.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IReceiptVerifier { + enum ReceiptStatus { + Unknown, + Submitted + } + + struct ReceiptReport { + address reporter; + bytes32 observationId; + bytes32 rootfieldId; + bytes32 receiptCommitment; + bytes32 reportHash; + ReceiptStatus status; + uint64 submittedAt; + } + + event ReceiptReportSubmitted( + bytes32 indexed reportId, + address indexed reporter, + bytes32 indexed observationId, + bytes32 rootfieldId, + bytes32 receiptCommitment, + bytes32 reportHash, + string evidenceURI + ); + + function submitReceiptReport( + bytes32 reportId, + bytes32 observationId, + bytes32 rootfieldId, + bytes32 receiptCommitment, + bytes32 reportHash, + string calldata evidenceURI + ) external; + + function getReceiptReport(bytes32 reportId) external view returns (ReceiptReport memory); +} diff --git a/contracts/interfaces/IRootfieldRegistry.sol b/contracts/interfaces/IRootfieldRegistry.sol new file mode 100644 index 00000000..699bfdb0 --- /dev/null +++ b/contracts/interfaces/IRootfieldRegistry.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IRootfieldRegistry { + function registerRootfield( + bytes32 rootfieldId, + bytes32 schemaHash, + bytes32 metadataHash, + string calldata metadataURI + ) external returns (bytes32 pulseId); + + function submitRoot( + bytes32 rootfieldId, + bytes32 root, + bytes32 artifactCommitment, + bytes32 parentPulseId, + string calldata evidenceURI + ) external returns (bytes32 pulseId); + + function deactivateRootfield(bytes32 rootfieldId, bytes32 parentPulseId, string calldata reasonURI) + external + returns (bytes32 pulseId); + + function transferRootfieldOwnership(bytes32 rootfieldId, address newOwner, string calldata evidenceURI) + external + returns (bytes32 pulseId); + + function isRegistered(bytes32 rootfieldId) external view returns (bool); +} diff --git a/contracts/interfaces/IVerifierRegistry.sol b/contracts/interfaces/IVerifierRegistry.sol new file mode 100644 index 00000000..441490fa --- /dev/null +++ b/contracts/interfaces/IVerifierRegistry.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IVerifierRegistry { + enum VerifierStatus { + Unknown, + Active, + Inactive + } + + struct Verifier { + bytes32 operatorId; + bytes32 role; + bytes32 metadataHash; + VerifierStatus status; + uint64 registeredAt; + uint64 updatedAt; + uint64 updateCount; + bool active; + } + + event VerifierRegistered( + address indexed verifier, + bytes32 indexed operatorId, + bytes32 indexed role, + bytes32 metadataHash, + string metadataURI + ); + event VerifierMetadataUpdated( + address indexed verifier, + bytes32 indexed operatorId, + bytes32 metadataHash, + uint64 updateCount, + string metadataURI + ); + event VerifierDeactivated( + address indexed verifier, bytes32 indexed operatorId, bytes32 reasonHash, string evidenceURI + ); + + function registerVerifier(bytes32 operatorId, bytes32 role, bytes32 metadataHash, string calldata metadataURI) + external; + function updateVerifierMetadata(bytes32 metadataHash, string calldata metadataURI) external; + function deactivateVerifier(bytes32 reasonHash, string calldata evidenceURI) external; + function getVerifier(address verifier) external view returns (Verifier memory); +} diff --git a/contracts/interfaces/IVerifierReportRegistry.sol b/contracts/interfaces/IVerifierReportRegistry.sol new file mode 100644 index 00000000..06d6ec4d --- /dev/null +++ b/contracts/interfaces/IVerifierReportRegistry.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IVerifierReportRegistry { + struct VerifierReport { + address verifier; + bytes32 rootfieldId; + bytes32 receiptId; + uint8 status; + bytes32 reportDigest; + bytes32 evidenceCommitment; + uint64 submittedAt; + bool exists; + } + + event VerifierAuthorizationSet(address indexed verifier, bool authorized); + event VerifierReportSubmitted( + bytes32 indexed reportId, + address indexed verifier, + bytes32 indexed receiptId, + bytes32 rootfieldId, + uint8 status, + bytes32 reportDigest, + bytes32 evidenceCommitment, + string evidenceURI + ); + + function setVerifierAuthorization(address verifier, bool authorized) external; + + function submitVerifierReport( + bytes32 reportId, + bytes32 rootfieldId, + bytes32 receiptId, + uint8 status, + bytes32 reportDigest, + bytes32 evidenceCommitment, + string calldata evidenceURI + ) external; + + function isAuthorizedVerifier(address verifier) external view returns (bool); + function getVerifierReport(bytes32 reportId) external view returns (VerifierReport memory); +} diff --git a/contracts/interfaces/IWorkDebtScheduler.sol b/contracts/interfaces/IWorkDebtScheduler.sol new file mode 100644 index 00000000..74875b04 --- /dev/null +++ b/contracts/interfaces/IWorkDebtScheduler.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IWorkDebtScheduler { + enum WorkStatus { + Unknown, + Scheduled, + Completed + } + + struct WorkItem { + address scheduler; + address worker; + bytes32 rootfieldId; + bytes32 workCommitment; + bytes32 metadataHash; + WorkStatus status; + uint64 scheduledAt; + uint64 updatedAt; + } + + event WorkScheduled( + bytes32 indexed workId, + address indexed scheduler, + address indexed worker, + bytes32 rootfieldId, + bytes32 workCommitment, + bytes32 metadataHash, + string workURI + ); + event WorkCompleted( + bytes32 indexed workId, + address indexed caller, + bytes32 completionCommitment, + bytes32 metadataHash, + string evidenceURI + ); + + function scheduleWork( + bytes32 workId, + address worker, + bytes32 rootfieldId, + bytes32 workCommitment, + bytes32 metadataHash, + string calldata workURI + ) external; + + function markWorkComplete( + bytes32 workId, + bytes32 completionCommitment, + bytes32 metadataHash, + string calldata evidenceURI + ) external; + + function getWorkItem(bytes32 workId) external view returns (WorkItem memory); +} diff --git a/contracts/interfaces/IWorkReceiptRegistry.sol b/contracts/interfaces/IWorkReceiptRegistry.sol new file mode 100644 index 00000000..98df8ea8 --- /dev/null +++ b/contracts/interfaces/IWorkReceiptRegistry.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IWorkReceiptRegistry { + struct WorkReceipt { + address worker; + bytes32 rootfieldId; + uint8 lane; + bytes32 subject; + bytes32 inputRoot; + bytes32 outputRoot; + bytes32 artifactCommitment; + bytes32 parentReceiptId; + uint64 submittedAt; + bool exists; + } + + event WorkerAuthorizationSet(address indexed worker, bool authorized); + event WorkReceiptSubmitted( + bytes32 indexed receiptId, + address indexed worker, + bytes32 indexed rootfieldId, + uint8 lane, + bytes32 subject, + bytes32 inputRoot, + bytes32 outputRoot, + bytes32 artifactCommitment, + bytes32 parentReceiptId, + string evidenceURI + ); + + function setWorkerAuthorization(address worker, bool authorized) external; + + function submitWorkReceipt( + bytes32 receiptId, + bytes32 rootfieldId, + uint8 lane, + bytes32 subject, + bytes32 inputRoot, + bytes32 outputRoot, + bytes32 artifactCommitment, + bytes32 parentReceiptId, + string calldata evidenceURI + ) external; + + function isAuthorizedWorker(address worker) external view returns (bool); + function getWorkReceipt(bytes32 receiptId) external view returns (WorkReceipt memory); +} diff --git a/contracts/interfaces/IWorkerRegistry.sol b/contracts/interfaces/IWorkerRegistry.sol new file mode 100644 index 00000000..22309e7c --- /dev/null +++ b/contracts/interfaces/IWorkerRegistry.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +interface IWorkerRegistry { + enum WorkerStatus { + Unknown, + Active, + Inactive + } + + struct Worker { + bytes32 operatorId; + bytes32 role; + bytes32 metadataHash; + WorkerStatus status; + uint64 registeredAt; + uint64 updatedAt; + uint64 updateCount; + bool active; + } + + event WorkerRegistered( + address indexed worker, + bytes32 indexed operatorId, + bytes32 indexed role, + bytes32 metadataHash, + string metadataURI + ); + event WorkerMetadataUpdated( + address indexed worker, bytes32 indexed operatorId, bytes32 metadataHash, uint64 updateCount, string metadataURI + ); + event WorkerDeactivated(address indexed worker, bytes32 indexed operatorId, bytes32 reasonHash, string evidenceURI); + + function registerWorker(bytes32 operatorId, bytes32 role, bytes32 metadataHash, string calldata metadataURI) + external; + function updateWorkerMetadata(bytes32 metadataHash, string calldata metadataURI) external; + function deactivateWorker(bytes32 reasonHash, string calldata evidenceURI) external; + function getWorker(address worker) external view returns (Worker memory); +} diff --git a/docs/DECISIONS/2026-05-12-flowpulse-observation-identity.md b/docs/DECISIONS/2026-05-12-flowpulse-observation-identity.md new file mode 100644 index 00000000..8fb58d9b --- /dev/null +++ b/docs/DECISIONS/2026-05-12-flowpulse-observation-identity.md @@ -0,0 +1,122 @@ +# FlowPulse Observation Identity + +Date: 2026-05-12 + +## Status + +Accepted for the MVP foundation. + +## Context + +FlowPulse contracts emit a `pulseId`, but contracts cannot know final receipt metadata such as `txHash`, `transactionIndex`, `logIndex`, `blockNumber`, or `blockHash` during execution. This is especially important for future Uniswap v4 hook work: hooks can emit events, but they cannot know final transaction metadata while running. + +The indexer needs a canonical identity for an observed FlowPulse log after receipts/logs are available. The identity must distinguish reorged occurrences, support deterministic verifier reports, and avoid treating advisory URI data as trusted evidence. + +## Decision + +FlowMemory will use three separate identifiers in the indexer/verifier MVP: + +- `pulseId`: emitted by the contract inside the FlowPulse event. +- `observationId`: derived by the indexer from receipt/log metadata after execution. +- `reportId`: derived by the verifier from canonical JSON report content. + +`pulseId` is protocol data, not canonical observation identity. The canonical indexer identity is `observationId`. + +## FlowPulse Event Signature + +The v0 event signature is: + +```text +FlowPulse(bytes32,bytes32,address,uint8,bytes32,bytes32,bytes32,uint64,uint64,string) +``` + +The `topic0` hash is: + +```text +0x5d07190b9ae441b4d7b16259a48424acd451492b12f5f99a29f5bfd992c13e43 +``` + +## Observation ID + +The indexer derives `observationId` from: + +- Domain: `flowmemory.flowpulse.observation.v0` +- `chainId` +- `emittingContract` +- `eventSignature` +- `blockNumber` +- `blockHash` +- `txHash` +- `transactionIndex` +- `logIndex` + +Preimage: + +```text +keccak256(abi.encode( + "flowmemory.flowpulse.observation.v0", + chainId, + emittingContract, + eventSignature, + blockNumber, + blockHash, + txHash, + transactionIndex, + logIndex +)) +``` + +`blockHash` and `blockNumber` are included so a reorged log occurrence and a later re-mined occurrence can be represented as distinct observations. `eventSignature` is included so the identity is explicitly scoped to FlowPulse v0 logs, not arbitrary logs at the same receipt location. + +Decoded fields such as `pulseId`, `rootfieldId`, `actor`, `pulseType`, `subject`, `commitment`, `parentPulseId`, `sequence`, `occurredAt`, and `uri` are stored with the observation but are not part of the identity preimage. If the same `observationId` is observed with different decoded fields, that is an indexer integrity failure. + +## Lifecycle Names + +- `pending`: candidate scan work or pre-receipt/pre-finality context. +- `mined`: successful receipt contains a decodable FlowPulse log and `observationId` exists. +- `finalized`: mined observation remains canonical after the configured finality policy. +- `reorged`: observation block/log is no longer canonical or was marked removed. + +The MVP does not require mempool indexing. A canonical `observationId` begins at `mined`. + +## Duplicate Names + +- Exact duplicate: same `observationId` and same decoded content; idempotent replay. +- Conflicting duplicate: same `observationId` but changed decoded content or metadata; indexer integrity failure. +- Pulse duplicate: same contract-emitted `pulseId` but different `observationId`; separate observations that require verifier/operator policy. +- Reorg replacement: different `observationId` caused by changed `blockHash`, block position, transaction position, or log position. + +## Report ID + +The verifier derives `reportId` from the canonical report body: + +```text +keccak256(canonical_json(reportCore)) +``` + +The `reportCore` includes `schema = flowmemory.verifier.report.v0`, `observationId`, observed receipt/log metadata, decoded FlowPulse fields, status, reason codes, resolver policy id, and verifier spec version. The report id does not include signatures, wall-clock generation timestamps, local file paths, or operator notes. + +## Consequences + +- Indexers can distinguish contract-emitted pulse identity from observed-log identity. +- Reorged observations remain addressable for audits without being treated as current canonical facts. +- Verifiers can produce deterministic reports bound to receipt/log facts. +- Dashboards and explorers can distinguish observed, verified, unresolved, unsupported, failed, reorged, stale, disputed, and superseded outcomes later without changing the observation identity. + +## Out Of Scope + +- Production indexer runtime. +- Live RPC integration. +- Database schema. +- Verifier economics. +- Proof network. +- Artifact canonicalization format. +- Resolver policy. +- Report signing or verifier attestation implementation. + +## Follow-Ups + +- Define artifact commitment canonicalization. +- Define resolver policy v0. +- Define Base finality policy. +- Decide whether future report attestations should use EIP-712 or another signature format. diff --git a/docs/DECISIONS/2026-05-12-flowpulse-v0.md b/docs/DECISIONS/2026-05-12-flowpulse-v0.md new file mode 100644 index 00000000..d89b4837 --- /dev/null +++ b/docs/DECISIONS/2026-05-12-flowpulse-v0.md @@ -0,0 +1,78 @@ +# FlowPulse v0 + +Date: 2026-05-12 + +## Status + +Accepted + +## Context + +FlowMemory needs an initial on-chain event surface for contract activity that indexers and verifiers can reconstruct from receipts and logs. The project is still in its contracts foundation stage, so the first version needs to be small enough to review safely while preserving the core protocol boundaries already documented in the repository. + +The initial contracts foundation includes a `FlowPulse` event schema and a `RootfieldRegistry` skeleton. This decision records the intended meaning and limits of that first schema so future contracts, indexers, and reviewers share the same assumptions. + +## Decision + +FlowPulse v0 is the first canonical event schema for FlowMemory protocol activity. It emits a domain-separated `pulseId`, a `rootfieldId`, the emitting actor, a compact pulse type, a type-specific subject, a type-specific commitment, an optional parent pulse id, a per-rootfield sequence, a block timestamp, and an advisory URI string. + +FlowPulse exists to provide a small, consistent event stream for commitment-oriented protocol actions such as registering a Rootfield namespace or committing a new root. It is not intended to store raw memory, model, media, artifact, or verification payloads. + +FlowPulse v0 intentionally excludes: + +- Dynamic fee mechanics +- Tokenomics +- Uniswap v4 hook behavior +- Upgrade policy +- Raw AI memory or model data +- Raw artifacts, media, or large evidence payloads +- Contract-claimed `txHash` or `logIndex` +- Enforced URI length, format, content, or resolver policy + +`txHash` and `logIndex` are indexer-derived because contracts cannot know final receipt metadata while executing. This is especially important for future hook work: a hook can emit events or update intentional state, but it cannot know the final transaction hash or log index during execution. Indexers and verifiers must derive those values after reading transaction receipts and logs. + +Heavy AI, memory, model, artifact, media, and evidence data stays off-chain because on-chain storage is expensive and inappropriate for large or sensitive payloads. FlowPulse v0 commits to off-chain facts with hashes and roots; it does not make the chain a data store for the underlying material. + +URI fields are advisory and unenforced in v0. `metadataURI` and `evidenceURI` are arbitrary caller-supplied strings accepted by the current skeleton contract. The contract does not enforce length, content, format, resolvability, or "short pointer" behavior, and emitted URI bytes are still on-chain log data. Keeping heavy or sensitive data out of URI fields is a caller responsibility and verifier policy, not a contract guarantee. + +FlowPulse is intentionally minimal so that the first contracts foundation can establish stable event semantics without prematurely committing to fee design, token design, hook integration, verifier economics, proof formats, or appchain assumptions. + +## Consequences + +FlowPulse v0 gives indexers and verifiers an initial stream to consume without requiring product features or full protocol mechanics. It also keeps the contracts foundation reviewable by limiting on-chain state to compact commitments and metadata needed by the registry skeleton. + +The minimal design leaves several responsibilities outside the contract: + +- Indexers must attach chain id, contract address, transaction hash, log index, block number, and receipt status from observed chain data. +- Verifiers must resolve and check off-chain content against emitted commitments. +- Callers must avoid placing heavy or sensitive raw data into URI strings. +- Future contracts must define any stronger validation or enforcement rules explicitly. + +## Future Versions May Add + +- Bounded `bytes32` commitment-only fields where URI strings are unnecessary +- CID or hash-only fields for stricter off-chain artifact references +- URI length caps or validation policy +- Additional FlowPulse type identifiers and type-specific semantics +- Rootflow-specific commitment semantics +- Attestation, receipt, proof, or verifier status events +- Access control and upgrade decisions if protocol governance requires them +- Uniswap v4 hook integration after hook-specific constraints are documented + +## Out Of Scope + +- Dynamic fees +- Token mechanics or incentives +- Production deployment policy +- Uniswap v4 hooks +- Full indexer or verifier implementation +- Cryptographic proof system design +- Appchain or L1 design +- Hardware signaling semantics +- Storing raw AI memory, model, artifact, media, or evidence data on-chain + +## Follow-Ups + +- Define the first indexer contract-event ingestion expectations. +- Decide whether future URI fields should be replaced or constrained by bounded commitments, CIDs, hashes, length caps, or validation rules. +- Define additional FlowPulse types only when a concrete contract or verifier workflow needs them. diff --git a/docs/DECISIONS/2026-05-12-hook-adapter-v0-boundary.md b/docs/DECISIONS/2026-05-12-hook-adapter-v0-boundary.md new file mode 100644 index 00000000..88521f53 --- /dev/null +++ b/docs/DECISIONS/2026-05-12-hook-adapter-v0-boundary.md @@ -0,0 +1,38 @@ +# FlowMemory Hook Adapter v0 Boundary + +Date: 2026-05-12 + +## Status + +Accepted + +## Context + +FlowMemory may later integrate with Uniswap v4 hooks on Base, but production hook behavior is not part of the Live V0 contracts package. The current package needs a compileable scaffold so future hook work has an explicit boundary without adding custom accounting, custody, dynamic fees, or deployment assumptions. + +## Decision + +Create `FlowMemoryHookAdapter` as a dependency-light scaffold with an `afterSwap`-style function. The adapter records a compact observation event containing caller, sender, pool id, rootfield id, commitment, and hook data hash. + +The adapter intentionally does not: + +- Import Uniswap v4 dependencies +- Implement production hook permissions +- Implement custom accounting +- Implement dynamic fees +- Hold or transfer tokens +- Call external protocols +- Know or claim `txHash` or `logIndex` +- Deploy to production networks + +Receipt metadata remains indexer-derived after transaction receipts and logs are available. + +## Consequences + +The scaffold gives contracts, tests, and CI a concrete hook-adjacent boundary while avoiding a false claim that FlowMemory has a production Uniswap v4 hook. Future hook work must replace or wrap this adapter with real Uniswap v4 interfaces only after dependency, permission, accounting, and deployment decisions are accepted. + +## Follow-Ups + +- Define the production Uniswap v4 hook adapter boundary in a separate issue before importing hook dependencies. +- Decide whether hook events should emit FlowPulse directly or be transformed by indexers. +- Add dependency-specific tests only after the project accepts concrete Uniswap v4 package versions. diff --git a/docs/DECISIONS/2026-05-12-rootfield-v0-boundary.md b/docs/DECISIONS/2026-05-12-rootfield-v0-boundary.md new file mode 100644 index 00000000..9c90d4f2 --- /dev/null +++ b/docs/DECISIONS/2026-05-12-rootfield-v0-boundary.md @@ -0,0 +1,93 @@ +# Rootfield v0 Boundary + +Date: 2026-05-12 + +## Status + +Accepted + +## Context + +The first contracts foundation introduces `RootfieldRegistry` as a minimal namespace and root-commitment skeleton. It needs a clear boundary before later agents add indexers, verifiers, hooks, fees, or governance mechanics. + +Rootfield v0 is deliberately small. It records ownership, schema and metadata hashes, the latest committed root, basic counters, and FlowPulse emissions. This decision defines what those fields mean in v0 and what they do not yet guarantee. + +## Decision + +Rootfield v0 is a commitment registry boundary, not a data-storage layer. Contract state should remain compact and should represent intentional commitments, not raw AI memory, model artifacts, media, or large evidence payloads. + +### URI Policy + +`metadataURI` and `evidenceURI` are advisory strings. The v0 contract accepts them as caller-supplied event data and does not enforce length, content, format, resolvability, allowed schemes, or "short pointer" behavior. Emitted URI bytes are still on-chain log data. + +Callers are responsible for keeping heavy or sensitive raw data out of URI fields. Verifiers and indexers may use URI values as hints, but they must not treat them as trusted facts without checking the relevant commitments. + +Future versions should decide whether URI handling remains advisory or moves to one of these stricter options: + +- Capped strings, if human-readable pointers remain useful +- `bytes32` commitments, if contract-level compactness is more important +- CID/hash-only fields, if artifact addressing needs deterministic validation +- A validation policy, if accepted schemes, lengths, or formats must be enforceable + +These alternatives are deliberately deferred in v0. Capped strings still leave content and resolver semantics ambiguous. `bytes32` commitments are compact but require canonical hashing rules that are not finalized. CID/hash-only fields may be the right artifact boundary, but the project has not yet chosen a storage or retrieval convention. URI validation policy needs clearer verifier and indexer requirements before it belongs in contract logic. + +### Commitment Semantics + +`schemaHash`, `metadataHash`, `latestRoot`, and `artifactCommitment` are compact commitments to off-chain definitions, metadata, root states, or evidence. They are not the underlying data. A commitment only becomes meaningful when an indexer or verifier can resolve the referenced artifact and check that it matches the emitted or stored hash. + +Rootfield v0 does not define the complete hashing standard for every off-chain artifact. Callers and verifiers must agree on artifact canonicalization before treating a commitment as verified protocol truth. + +### Status Lifecycle Assumptions + +The v0 registry lifecycle is intentionally narrow but now includes the minimum lifecycle controls needed for reviewable local testing: + +- A rootfield starts unregistered. +- Registration creates an active rootfield owned by the registering account. +- The owner may submit roots while the rootfield is active. +- Root submission updates `latestRoot`, increments counters, and emits FlowPulse. +- The owner may deactivate the rootfield. Deactivation emits a status-change FlowPulse and prevents later root submissions. +- The owner may transfer ownership of an active rootfield to another nonzero address. Transfer emits a status-change FlowPulse and the new owner becomes the only account that can submit roots or deactivate the rootfield. + +V0 does not implement reactivation, pause windows, challenge periods, governance-controlled status transitions, social recovery, multisig recovery, or automated dispute handling. Ownership transfer is a direct owner-only operation, not a recovery system. + +### Namespace Squatting Policy + +Rootfield v0 is first-come, first-served. The contract does not reserve names, charge namespace fees, check human-readable ownership, enforce allowlists, or prevent someone from registering a desirable rootfield id before another party. + +Callers should treat `rootfieldId` values as opaque commitments or collision-resistant identifiers rather than public brand names. Future versions may add reserved namespaces, governance review, attestations, signed claims, challenge windows, or fee-based anti-squatting policy, but none of that exists in v0. + +### Ownership And Recovery Policy + +Rootfield v0 supports only explicit owner-initiated transfer to a nonzero address. It does not support lost-key recovery, administrator rescue, timelocked transfer acceptance, role-based administration, guardians, or ownership disputes. + +This keeps the skeleton easy to audit but means a lost owner key can strand an active rootfield. Any production recovery design should be introduced as a separate reviewed protocol change. + +## v0 Intentionally Excludes + +- Tokenomics +- Dynamic fees +- Uniswap v4 hooks +- L1 or appchain mechanics +- Production deployment logic +- Lost-key recovery +- Governance or upgrade policy +- Reactivation after deactivation +- Pause windows or challenge periods +- Challenge, slashing, dispute, or verifier reward mechanics +- Contract-enforced URI validation +- Full artifact canonicalization rules +- Raw AI memory, model, artifact, media, or evidence storage + +## Consequences + +This boundary keeps Rootfield v0 reviewable and useful as a first protocol surface. It allows contract tests and CI to stabilize around registration, root commitment, and FlowPulse emission without forcing premature decisions about fees, hooks, governance, verifier economics, or artifact standards. + +The tradeoff is that important verification guarantees remain off-chain. Indexers and verifiers must derive receipt metadata, validate commitments, decide how to handle URI hints, and reject heavy or sensitive raw data by policy until the protocol adds stricter contract rules. + +## Follow-Ups + +- Decide whether Rootfield URI fields should become capped strings, `bytes32` commitments, CID/hash-only fields, or validated strings. +- Define canonical hashing and serialization rules for metadata, root states, and artifact evidence. +- Decide whether namespace squatting needs reserved ids, attestations, governance review, challenge windows, or fees. +- Decide whether ownership transfer should require acceptance, delay, multisig policy, or recovery hooks. +- Define any future rootfield reactivation, pause, challenge, or governance behavior as a separate reviewed change. diff --git a/docs/DECISIONS/2026-05-13-artifact-registry-v0-boundary.md b/docs/DECISIONS/2026-05-13-artifact-registry-v0-boundary.md new file mode 100644 index 00000000..b637414b --- /dev/null +++ b/docs/DECISIONS/2026-05-13-artifact-registry-v0-boundary.md @@ -0,0 +1,47 @@ +# Artifact Registry v0 Boundary + +Date: 2026-05-13 + +## Status + +Accepted + +## Context + +FlowMemory needs a contract surface for referencing artifacts without storing raw AI memory, model outputs, media, datasets, or large evidence payloads on-chain. + +## Decision + +ArtifactRegistry v0 stores compact artifact commitments only. A record binds an `artifactId` to: + +- `rootfieldId` +- `artifactType` +- `commitmentHash` +- `schemaHash` +- `metadataHash` +- `owner` and `submitter` +- lifecycle status + +The advisory `artifactURI` and `evidenceURI` fields are emitted as event strings only. They are not stored as canonical artifact data and are not validated for length, content, format, scheme, resolvability, or short-pointer behavior. + +## Commitment Semantics + +`commitmentHash`, `schemaHash`, and `metadataHash` are bytes32 commitments. V0 does not define canonical serialization, hashing, resolver policy, content addressing, or artifact availability guarantees. Verifiers must decide how to resolve and check off-chain artifact data against these commitments. + +## Status Lifecycle + +V0 supports `Active` and owner-only `Deprecated`. Deprecation is a signal that later consumers should prefer newer artifacts or policies. It is not a deletion, slashing event, dispute result, or proof of invalidity. + +## Intentionally Excluded + +- Raw artifact storage +- Contract-enforced URI validation +- Canonical CID/hash-only resolver policy +- Artifact availability guarantees +- Access control beyond artifact-owner deprecation +- Token staking, payments, rewards, or slashing +- Production audit claims + +## Future Options + +Future versions should decide whether artifacts move toward capped strings, CID/hash-only fields, stricter bytes32 commitment standards, resolver validation, or explicit canonicalization profiles. Those choices need verifier and indexer policy first. diff --git a/docs/DECISIONS/2026-05-13-work-receipt-verifier-report-v0-boundary.md b/docs/DECISIONS/2026-05-13-work-receipt-verifier-report-v0-boundary.md new file mode 100644 index 00000000..b13818da --- /dev/null +++ b/docs/DECISIONS/2026-05-13-work-receipt-verifier-report-v0-boundary.md @@ -0,0 +1,72 @@ +# Work Receipt And Verifier Report v0 Boundary + +Date: 2026-05-13 + +## Status + +Accepted + +## Context + +FlowMemory needs minimal contract surfaces for compact work receipts and verifier reports. These contracts should support local testing and indexer integration without pretending to provide a complete verifier network. + +## Decision + +WorkReceiptRegistry v0 stores compact work receipt commitments from owner-authorized workers. A receipt binds: + +- `rootfieldId` +- work lane +- subject +- input and output roots +- artifact commitment +- optional parent receipt id + +VerifierReportRegistry v0 stores compact verifier report commitments from owner-authorized verifiers. A report binds: + +- rootfield id or receipt id +- report status +- report digest +- evidence commitment +- verifier address + +Both registries emit advisory URI strings as on-chain log data only. URI length, content, format, and resolver behavior are not enforced. + +## Work Lanes + +The initial lanes are: + +- `MEMORY_REFRESH` +- `FAILURE_DISCOVERY` +- `FAILURE_REPAIR` +- `MANIFOLD_DISCOVERY` +- `STEERING_VALIDATION` +- `CHECKPOINT_STORAGE` +- `GPU_TRAINING` +- `EVAL_COUNTEREXAMPLE` + +The lane value is a categorization commitment. It does not prove work quality, priority, payment, or scheduler correctness. + +## Report Statuses + +VerifierReportRegistry v0 accepts `VALID`, `INVALID`, `UNRESOLVED`, `UNSUPPORTED`, and `REORGED`. These are verifier-submitted claims, not on-chain proof outcomes. The contract does not cryptographically verify receipts, artifacts, model outputs, or chain reorgs. + +## Authorization Semantics + +V0 uses local owner-controlled allowlists for workers and verifiers. This is intentionally simple and reviewable. It is not decentralized governance, staking, Sybil resistance, or a production verifier network. + +## Intentionally Excluded + +- Tokenomics +- Dynamic fees +- Rewards +- Slashing +- External protocol calls +- On-chain receipt verification +- Raw artifact or evidence storage +- Production hook integration +- Mainnet deployment assumptions +- Production audit claims + +## Future Options + +Future versions should decide whether authorization connects to WorkerRegistry and VerifierRegistry, whether report evidence moves to CID/hash-only fields, whether work receipts emit FlowPulse directly, and whether verifier adapters need stronger proof systems or signature formats. diff --git a/docs/DECISIONS/2026-05-13-worker-verifier-registry-v0-boundary.md b/docs/DECISIONS/2026-05-13-worker-verifier-registry-v0-boundary.md new file mode 100644 index 00000000..104f490a --- /dev/null +++ b/docs/DECISIONS/2026-05-13-worker-verifier-registry-v0-boundary.md @@ -0,0 +1,47 @@ +# Worker And Verifier Registry v0 Boundary + +Date: 2026-05-13 + +## Status + +Accepted + +## Context + +FlowMemory needs identity surfaces for workers and verifiers before building scheduling, receipt, report, or hook integrations. V0 should make identities observable without implying economics, Sybil resistance, or production trust. + +## Decision + +WorkerRegistry and VerifierRegistry v0 are self-registration registries. An address registers its own `operatorId`, `role`, and metadata commitment, and may later update metadata or deactivate itself. + +Registration does not prove real-world identity, competence, stake, reputation, uniqueness, or authorization to earn rewards. It is a lightweight on-chain identity and metadata commitment surface. + +## Authorization Semantics + +Self-registration is not the same as permission to submit work receipts or verifier reports. WorkReceiptRegistry and VerifierReportRegistry use their own owner-controlled allowlists in v0. Future versions may connect these registries, require attestations, or introduce governance-controlled authorization. + +## Status Lifecycle + +V0 supports: + +- unregistered +- active after self-registration +- inactive after self-deactivation + +Inactive records remain historical records. V0 does not support reactivation, delegated administration, third-party suspension, governance slashing, or recovery. + +## Intentionally Excluded + +- Token staking +- Rewards +- Slashing +- Reputation scoring +- Sybil resistance +- Real-world identity verification +- Delegated updates +- Governance-managed allowlists +- Production audit claims + +## Future Options + +Future versions should decide whether worker and verifier authorization uses allowlists, signed attestations, staking, governance, decentralized identity, or links to off-chain indexer identity specs. diff --git a/docs/SECURITY_MODEL.md b/docs/SECURITY_MODEL.md index d6ae24c9..4e46e833 100644 --- a/docs/SECURITY_MODEL.md +++ b/docs/SECURITY_MODEL.md @@ -48,6 +48,19 @@ Follow-up: - Consider bounded `bytes32` commitments, CID/hash-only fields, URI length caps, or a URI validation policy before treating this skeleton as an enforceable off-chain-data boundary. +#### Live V0 Contract Skeletons + +Live V0 registries and schedulers are commitment surfaces, not complete trust systems: + +- CursorRegistry stores cursor commitments but does not define canonical indexer identity or reorg policy. +- ReceiptVerifier stores receipt report commitments but does not cryptographically verify receipts on-chain. +- WorkerRegistry and VerifierRegistry are self-registration surfaces without staking, rewards, slashing, Sybil resistance, or production authorization guarantees. +- ArtifactRegistry stores artifact commitments, type/schema/metadata hashes, owner, submitter, and status only; raw artifacts and sensitive payloads remain off-chain. +- WorkReceiptRegistry and VerifierReportRegistry use owner-controlled allowlists in v0. Those allowlists are local testing policy, not decentralized governance or a production verifier network. +- WorkDebtScheduler stores compact work state without token debt, dynamic fees, rewards, or external calls. +- FlowMemoryHookAdapter is a compileable scaffold only; it is not a production Uniswap v4 hook and cannot know `txHash` or `logIndex`. +- These contracts are not production audited and are not mainnet-ready. + ### Indexers And Verifiers - Log parsing errors @@ -55,6 +68,10 @@ Follow-up: - Incorrect `txHash` or `logIndex` derivation - Non-deterministic verification output - Trusting off-chain artifacts without checking commitments +- Treat contract-emitted `pulseId` as protocol payload, not canonical observed-log identity. +- Bind `observationId` to receipt/log metadata, including chain id, emitting contract, FlowPulse event signature, block hash, transaction hash, transaction index, and log index. +- Treat advisory URI fields as lookup hints only; verifier reports must use explicit resolver policy and deterministic commitment checks. +- Do not store secrets, RPC credentials, API keys, seed phrases, or webhook URLs in indexer/verifier env files. ### AI Memory @@ -79,6 +96,12 @@ Follow-up: - CI secrets exposure - Binary artifacts without provenance +Static analysis preparation: + +- Slither was not available in the local PATH during the Live V0 package pass. +- Track setup in GitHub issue #24 before adding a CI gate. +- Candidate command once installed: `slither . --filter-paths "tests|script"`. + ## PR Security Checklist - Does this change introduce or require secrets? diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 00000000..b7b8c1bb --- /dev/null +++ b/foundry.toml @@ -0,0 +1,9 @@ +[profile.default] +src = "contracts" +test = "tests" +out = "out" +cache_path = "cache" +libs = [] +solc_version = "0.8.24" +optimizer = true +optimizer_runs = 200 diff --git a/tests/LiveV0Package.t.sol b/tests/LiveV0Package.t.sol new file mode 100644 index 00000000..747c4f03 --- /dev/null +++ b/tests/LiveV0Package.t.sol @@ -0,0 +1,614 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {ArtifactRegistry} from "../contracts/ArtifactRegistry.sol"; +import {CursorRegistry} from "../contracts/CursorRegistry.sol"; +import {FlowMemoryHookAdapter} from "../contracts/FlowMemoryHookAdapter.sol"; +import {ReceiptVerifier} from "../contracts/ReceiptVerifier.sol"; +import {VerifierReportRegistry} from "../contracts/VerifierReportRegistry.sol"; +import {VerifierRegistry} from "../contracts/VerifierRegistry.sol"; +import {WorkerRegistry} from "../contracts/WorkerRegistry.sol"; +import {WorkDebtScheduler} from "../contracts/WorkDebtScheduler.sol"; +import {WorkReceiptRegistry} from "../contracts/WorkReceiptRegistry.sol"; +import {IArtifactRegistry} from "../contracts/interfaces/IArtifactRegistry.sol"; +import {IReceiptVerifier} from "../contracts/interfaces/IReceiptVerifier.sol"; +import {IVerifierRegistry} from "../contracts/interfaces/IVerifierRegistry.sol"; +import {IWorkDebtScheduler} from "../contracts/interfaces/IWorkDebtScheduler.sol"; +import {IWorkerRegistry} from "../contracts/interfaces/IWorkerRegistry.sol"; + +interface LiveV0Vm { + struct Log { + bytes32[] topics; + bytes data; + address emitter; + } + + function expectRevert(bytes4 revertData) external; + function expectRevert(bytes calldata revertData) external; + function recordLogs() external; + function getRecordedLogs() external returns (Log[] memory); +} + +contract CursorRegistryCaller { + function advanceCursor(CursorRegistry registry, bytes32 cursorId, bytes32 positionCommitment) external { + registry.advanceCursor(cursorId, positionCommitment, keccak256("metadata.v2"), "cursor://evidence"); + } +} + +contract ArtifactRegistryCaller { + function deprecateArtifact(ArtifactRegistry registry, bytes32 artifactId) external { + registry.deprecateArtifact(artifactId, keccak256("artifact.deprecated"), "artifact://deprecated"); + } +} + +contract WorkerRegistryCaller { + function updateWorkerMetadata(WorkerRegistry registry) external { + registry.updateWorkerMetadata(keccak256("worker.metadata.v2"), "worker://metadata-v2"); + } +} + +contract VerifierRegistryCaller { + function updateVerifierMetadata(VerifierRegistry registry) external { + registry.updateVerifierMetadata(keccak256("verifier.metadata.v2"), "verifier://metadata-v2"); + } +} + +contract WorkReceiptRegistryCaller { + function setWorkerAuthorization(WorkReceiptRegistry registry, address worker, bool authorized) external { + registry.setWorkerAuthorization(worker, authorized); + } + + function submitWorkReceipt(WorkReceiptRegistry registry, bytes32 receiptId, uint8 lane) external { + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.alpha"), + lane, + keccak256("subject.alpha"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "work://evidence" + ); + } +} + +contract VerifierReportRegistryCaller { + function setVerifierAuthorization(VerifierReportRegistry registry, address verifier, bool authorized) external { + registry.setVerifierAuthorization(verifier, authorized); + } + + function submitVerifierReport(VerifierReportRegistry registry, bytes32 reportId, uint8 status) external { + registry.submitVerifierReport( + reportId, + keccak256("rootfield.alpha"), + keccak256("receipt.alpha"), + status, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "verifier://evidence" + ); + } +} + +contract WorkDebtSchedulerCaller { + function markWorkComplete(WorkDebtScheduler scheduler, bytes32 workId) external { + scheduler.markWorkComplete(workId, keccak256("completion"), keccak256("metadata"), "work://evidence"); + } +} + +contract LiveV0PackageTest { + LiveV0Vm private constant vm = LiveV0Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + error AssertionFailed(); + + function testCursorRegistryRegistersAndAdvancesCommitments() public { + CursorRegistry registry = new CursorRegistry(); + bytes32 cursorId = keccak256("cursor.alpha"); + bytes32 streamId = keccak256("stream.alpha"); + + registry.registerCursor(cursorId, streamId, keccak256("position.1"), keccak256("metadata.1"), "cursor://meta"); + registry.advanceCursor(cursorId, keccak256("position.2"), keccak256("metadata.2"), "cursor://evidence"); + + CursorRegistry.Cursor memory cursor = registry.getCursor(cursorId); + _assertTrue(cursor.owner == address(this)); + _assertTrue(cursor.streamId == streamId); + _assertTrue(cursor.positionCommitment == keccak256("position.2")); + _assertTrue(cursor.metadataHash == keccak256("metadata.2")); + _assertTrue(cursor.updateCount == 2); + _assertTrue(cursor.active); + } + + function testCursorRegistryRejectsDuplicateAndNonOwnerAdvance() public { + CursorRegistry registry = new CursorRegistry(); + bytes32 cursorId = keccak256("cursor.beta"); + registry.registerCursor(cursorId, keccak256("stream.beta"), keccak256("position.1"), keccak256("metadata"), ""); + + vm.expectRevert(abi.encodeWithSelector(CursorRegistry.CursorAlreadyRegistered.selector, cursorId)); + registry.registerCursor(cursorId, keccak256("stream.beta"), keccak256("position.1"), keccak256("metadata"), ""); + + CursorRegistryCaller caller = new CursorRegistryCaller(); + vm.expectRevert(abi.encodeWithSelector(CursorRegistry.NotCursorOwner.selector, cursorId, address(caller))); + caller.advanceCursor(registry, cursorId, keccak256("position.2")); + } + + function testWorkerAndVerifierRegistriesStoreSelfRegisteredMetadata() public { + WorkerRegistry workers = new WorkerRegistry(); + VerifierRegistry verifiers = new VerifierRegistry(); + + workers.registerWorker( + keccak256("worker.operator"), keccak256("worker.role"), keccak256("worker.metadata"), "worker://meta" + ); + verifiers.registerVerifier( + keccak256("verifier.operator"), + keccak256("verifier.role"), + keccak256("verifier.metadata"), + "verifier://meta" + ); + + WorkerRegistry.Worker memory worker = workers.getWorker(address(this)); + VerifierRegistry.Verifier memory verifier = verifiers.getVerifier(address(this)); + + _assertTrue(worker.operatorId == keccak256("worker.operator")); + _assertTrue(worker.role == keccak256("worker.role")); + _assertTrue(worker.metadataHash == keccak256("worker.metadata")); + _assertTrue(worker.status == IWorkerRegistry.WorkerStatus.Active); + _assertTrue(worker.updateCount == 1); + _assertTrue(worker.active); + + _assertTrue(verifier.operatorId == keccak256("verifier.operator")); + _assertTrue(verifier.role == keccak256("verifier.role")); + _assertTrue(verifier.metadataHash == keccak256("verifier.metadata")); + _assertTrue(verifier.status == IVerifierRegistry.VerifierStatus.Active); + _assertTrue(verifier.updateCount == 1); + _assertTrue(verifier.active); + } + + function testWorkerAndVerifierRegistriesDeactivateAndRejectUnregisteredUpdates() public { + WorkerRegistry workers = new WorkerRegistry(); + VerifierRegistry verifiers = new VerifierRegistry(); + WorkerRegistryCaller workerCaller = new WorkerRegistryCaller(); + VerifierRegistryCaller verifierCaller = new VerifierRegistryCaller(); + + vm.expectRevert(abi.encodeWithSelector(WorkerRegistry.WorkerNotRegistered.selector, address(workerCaller))); + workerCaller.updateWorkerMetadata(workers); + + vm.expectRevert( + abi.encodeWithSelector(VerifierRegistry.VerifierNotRegistered.selector, address(verifierCaller)) + ); + verifierCaller.updateVerifierMetadata(verifiers); + + workers.registerWorker( + keccak256("worker.operator"), keccak256("worker.role"), keccak256("worker.metadata"), "worker://meta" + ); + verifiers.registerVerifier( + keccak256("verifier.operator"), + keccak256("verifier.role"), + keccak256("verifier.metadata"), + "verifier://meta" + ); + + workers.deactivateWorker(keccak256("worker.reason"), "worker://inactive"); + verifiers.deactivateVerifier(keccak256("verifier.reason"), "verifier://inactive"); + + WorkerRegistry.Worker memory worker = workers.getWorker(address(this)); + VerifierRegistry.Verifier memory verifier = verifiers.getVerifier(address(this)); + + _assertTrue(worker.status == IWorkerRegistry.WorkerStatus.Inactive); + _assertTrue(!worker.active); + _assertTrue(worker.updateCount == 2); + _assertTrue(verifier.status == IVerifierRegistry.VerifierStatus.Inactive); + _assertTrue(!verifier.active); + _assertTrue(verifier.updateCount == 2); + + vm.expectRevert(abi.encodeWithSelector(WorkerRegistry.WorkerNotActive.selector, address(this))); + workers.updateWorkerMetadata(keccak256("worker.metadata.v2"), "worker://metadata-v2"); + + vm.expectRevert(abi.encodeWithSelector(VerifierRegistry.VerifierNotActive.selector, address(this))); + verifiers.updateVerifierMetadata(keccak256("verifier.metadata.v2"), "verifier://metadata-v2"); + } + + function testArtifactRegistryStoresCommitmentAndEmitsAdvisoryUri() public { + ArtifactRegistry registry = new ArtifactRegistry(); + bytes32 artifactId = keccak256("artifact.alpha"); + string memory artifactURI = "artifact://advisory-log-data"; + + vm.recordLogs(); + registry.registerArtifact( + artifactId, + keccak256("rootfield.alpha"), + keccak256("artifact.type"), + keccak256("artifact.commitment"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + artifactURI + ); + LiveV0Vm.Log[] memory logs = vm.getRecordedLogs(); + + ArtifactRegistry.Artifact memory artifact = registry.getArtifact(artifactId); + _assertTrue(artifact.owner == address(this)); + _assertTrue(artifact.submitter == address(this)); + _assertTrue(artifact.rootfieldId == keccak256("rootfield.alpha")); + _assertTrue(artifact.artifactType == keccak256("artifact.type")); + _assertTrue(artifact.commitmentHash == keccak256("artifact.commitment")); + _assertTrue(artifact.status == IArtifactRegistry.ArtifactStatus.Active); + _assertTrue(artifact.exists); + _assertTrue(logs.length == 1); + _assertTrue(logs[0].emitter == address(registry)); + } + + function testArtifactRegistryRejectsDuplicateAndInvalidCommitments() public { + ArtifactRegistry registry = new ArtifactRegistry(); + bytes32 artifactId = keccak256("artifact.beta"); + + vm.expectRevert(ArtifactRegistry.ZeroCommitmentHash.selector); + registry.registerArtifact( + artifactId, + keccak256("rootfield.beta"), + keccak256("artifact.type"), + bytes32(0), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + + registry.registerArtifact( + artifactId, + keccak256("rootfield.beta"), + keccak256("artifact.type"), + keccak256("artifact.commitment"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(abi.encodeWithSelector(ArtifactRegistry.ArtifactAlreadyRegistered.selector, artifactId)); + registry.registerArtifact( + artifactId, + keccak256("rootfield.beta"), + keccak256("artifact.type"), + keccak256("artifact.commitment.v2"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + } + + function testArtifactRegistryDeprecatesOnlyByOwner() public { + ArtifactRegistry registry = new ArtifactRegistry(); + ArtifactRegistryCaller caller = new ArtifactRegistryCaller(); + bytes32 artifactId = keccak256("artifact.gamma"); + + registry.registerArtifact( + artifactId, + keccak256("rootfield.gamma"), + keccak256("artifact.type"), + keccak256("artifact.commitment"), + keccak256("schema.hash"), + keccak256("metadata.hash"), + "" + ); + + vm.expectRevert(abi.encodeWithSelector(ArtifactRegistry.NotArtifactOwner.selector, artifactId, address(caller))); + caller.deprecateArtifact(registry, artifactId); + + registry.deprecateArtifact(artifactId, keccak256("artifact.deprecated"), "artifact://deprecated"); + ArtifactRegistry.Artifact memory artifact = registry.getArtifact(artifactId); + _assertTrue(artifact.status == IArtifactRegistry.ArtifactStatus.Deprecated); + _assertTrue(artifact.metadataHash == keccak256("artifact.deprecated")); + + vm.expectRevert(abi.encodeWithSelector(ArtifactRegistry.ArtifactNotActive.selector, artifactId)); + registry.deprecateArtifact(artifactId, keccak256("artifact.deprecated.again"), ""); + } + + function testReceiptVerifierStoresReportCommitmentWithoutReceiptMetadataClaims() public { + ReceiptVerifier verifier = new ReceiptVerifier(); + bytes32 reportId = keccak256("report.alpha"); + + verifier.submitReceiptReport( + reportId, + keccak256("observation.id"), + keccak256("rootfield.alpha"), + keccak256("receipt.commitment"), + keccak256("report.hash"), + "receipt://evidence" + ); + + ReceiptVerifier.ReceiptReport memory report = verifier.getReceiptReport(reportId); + _assertTrue(report.reporter == address(this)); + _assertTrue(report.observationId == keccak256("observation.id")); + _assertTrue(report.receiptCommitment == keccak256("receipt.commitment")); + _assertTrue(report.status == IReceiptVerifier.ReceiptStatus.Submitted); + + bytes32 signatureWithReceiptMetadata = + keccak256("ReceiptReportSubmitted(bytes32,address,bytes32,bytes32,bytes32,bytes32,string,bytes32,uint256)"); + bytes32 signatureWithoutReceiptMetadata = + keccak256("ReceiptReportSubmitted(bytes32,address,bytes32,bytes32,bytes32,bytes32,string)"); + _assertTrue(signatureWithReceiptMetadata != signatureWithoutReceiptMetadata); + } + + function testReceiptVerifierRejectsDuplicateReport() public { + ReceiptVerifier verifier = new ReceiptVerifier(); + bytes32 reportId = keccak256("report.dup"); + verifier.submitReceiptReport( + reportId, + keccak256("observation.id"), + keccak256("rootfield.alpha"), + keccak256("receipt.commitment"), + keccak256("report.hash"), + "" + ); + + vm.expectRevert(abi.encodeWithSelector(ReceiptVerifier.ReceiptReportAlreadySubmitted.selector, reportId)); + verifier.submitReceiptReport( + reportId, + keccak256("observation.id"), + keccak256("rootfield.alpha"), + keccak256("receipt.commitment"), + keccak256("report.hash"), + "" + ); + } + + function testWorkDebtSchedulerSchedulesAndCompletesWithoutTokenMechanics() public { + WorkDebtScheduler scheduler = new WorkDebtScheduler(); + bytes32 workId = keccak256("work.alpha"); + + scheduler.scheduleWork( + workId, + address(this), + keccak256("rootfield.alpha"), + keccak256("work.commitment"), + keccak256("metadata.hash"), + "work://advisory" + ); + scheduler.markWorkComplete(workId, keccak256("completion"), keccak256("metadata.done"), "work://evidence"); + + WorkDebtScheduler.WorkItem memory item = scheduler.getWorkItem(workId); + _assertTrue(item.scheduler == address(this)); + _assertTrue(item.worker == address(this)); + _assertTrue(item.workCommitment == keccak256("completion")); + _assertTrue(item.metadataHash == keccak256("metadata.done")); + _assertTrue(item.status == IWorkDebtScheduler.WorkStatus.Completed); + } + + function testWorkDebtSchedulerRejectsNonParticipantCompletion() public { + WorkDebtScheduler scheduler = new WorkDebtScheduler(); + bytes32 workId = keccak256("work.beta"); + address assignedWorker = address(0xBEEF); + + scheduler.scheduleWork( + workId, + assignedWorker, + keccak256("rootfield.beta"), + keccak256("work.commitment"), + keccak256("metadata.hash"), + "" + ); + + WorkDebtSchedulerCaller caller = new WorkDebtSchedulerCaller(); + vm.expectRevert(abi.encodeWithSelector(WorkDebtScheduler.NotWorkParticipant.selector, workId, address(caller))); + caller.markWorkComplete(scheduler, workId); + } + + function testWorkReceiptRegistryAuthorizesWorkerAndStoresLaneReceipt() public { + WorkReceiptRegistry registry = new WorkReceiptRegistry(); + bytes32 receiptId = keccak256("receipt.alpha"); + uint8 lane = registry.MEMORY_REFRESH(); + + registry.setWorkerAuthorization(address(this), true); + _assertTrue(registry.isAuthorizedWorker(address(this))); + + vm.recordLogs(); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.alpha"), + lane, + keccak256("subject.alpha"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "work://evidence" + ); + LiveV0Vm.Log[] memory logs = vm.getRecordedLogs(); + + WorkReceiptRegistry.WorkReceipt memory receipt = registry.getWorkReceipt(receiptId); + _assertTrue(receipt.worker == address(this)); + _assertTrue(receipt.rootfieldId == keccak256("rootfield.alpha")); + _assertTrue(receipt.lane == lane); + _assertTrue(receipt.inputRoot == keccak256("input.root")); + _assertTrue(receipt.outputRoot == keccak256("output.root")); + _assertTrue(receipt.artifactCommitment == keccak256("artifact.commitment")); + _assertTrue(receipt.exists); + _assertTrue(logs.length == 1); + _assertTrue(logs[0].emitter == address(registry)); + } + + function testWorkReceiptRegistryRejectsUnauthorizedInvalidLaneAndZeroRoots() public { + WorkReceiptRegistry registry = new WorkReceiptRegistry(); + bytes32 receiptId = keccak256("receipt.beta"); + uint8 memoryRefreshLane = registry.MEMORY_REFRESH(); + uint8 failureDiscoveryLane = registry.FAILURE_DISCOVERY(); + + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.WorkerNotAuthorized.selector, address(this))); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.beta"), + memoryRefreshLane, + keccak256("subject.beta"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + + registry.setWorkerAuthorization(address(this), true); + + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.InvalidWorkLane.selector, 9)); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.beta"), + 9, + keccak256("subject.beta"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + + vm.expectRevert(WorkReceiptRegistry.ZeroInputRoot.selector); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.beta"), + failureDiscoveryLane, + keccak256("subject.beta"), + bytes32(0), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + } + + function testWorkReceiptRegistryRejectsDuplicateReceipt() public { + WorkReceiptRegistry registry = new WorkReceiptRegistry(); + bytes32 receiptId = keccak256("receipt.dup"); + uint8 lane = registry.CHECKPOINT_STORAGE(); + + registry.setWorkerAuthorization(address(this), true); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.dup"), + lane, + keccak256("subject.dup"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + + vm.expectRevert(abi.encodeWithSelector(WorkReceiptRegistry.WorkReceiptAlreadySubmitted.selector, receiptId)); + registry.submitWorkReceipt( + receiptId, + keccak256("rootfield.dup"), + lane, + keccak256("subject.dup"), + keccak256("input.root"), + keccak256("output.root"), + keccak256("artifact.commitment"), + bytes32(0), + "" + ); + } + + function testVerifierReportRegistryAuthorizesVerifierAndStoresReport() public { + VerifierReportRegistry registry = new VerifierReportRegistry(); + bytes32 reportId = keccak256("verifier.report.alpha"); + uint8 status = registry.VALID(); + + registry.setVerifierAuthorization(address(this), true); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.alpha"), + keccak256("receipt.alpha"), + status, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "verifier://evidence" + ); + + VerifierReportRegistry.VerifierReport memory report = registry.getVerifierReport(reportId); + _assertTrue(report.verifier == address(this)); + _assertTrue(report.rootfieldId == keccak256("rootfield.alpha")); + _assertTrue(report.receiptId == keccak256("receipt.alpha")); + _assertTrue(report.status == status); + _assertTrue(report.reportDigest == keccak256("report.digest")); + _assertTrue(report.evidenceCommitment == keccak256("evidence.commitment")); + _assertTrue(report.exists); + } + + 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(); + + vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.VerifierNotAuthorized.selector, address(this))); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + validStatus, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + + registry.setVerifierAuthorization(address(this), true); + + vm.expectRevert(abi.encodeWithSelector(VerifierReportRegistry.InvalidReportStatus.selector, 0)); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + 0, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + unresolvedStatus, + keccak256("report.digest"), + keccak256("evidence.commitment"), + "" + ); + + vm.expectRevert( + abi.encodeWithSelector(VerifierReportRegistry.VerifierReportAlreadySubmitted.selector, reportId) + ); + registry.submitVerifierReport( + reportId, + keccak256("rootfield.beta"), + keccak256("receipt.beta"), + reorgedStatus, + keccak256("report.digest.2"), + keccak256("evidence.commitment.2"), + "" + ); + } + + function testFlowMemoryHookAdapterEmitsObservationAndReturnsSelector() public { + FlowMemoryHookAdapter adapter = new FlowMemoryHookAdapter(); + bytes memory hookData = abi.encode(keccak256("artifact.commitment")); + + vm.recordLogs(); + bytes4 selector = adapter.afterSwap( + address(this), keccak256("pool.alpha"), keccak256("rootfield.alpha"), keccak256("hook.commitment"), hookData + ); + LiveV0Vm.Log[] memory logs = vm.getRecordedLogs(); + + _assertTrue(selector == adapter.AFTER_SWAP_SELECTOR()); + _assertTrue(logs.length == 1); + _assertTrue(logs[0].emitter == address(adapter)); + } + + function testFlowMemoryHookAdapterRejectsZeroCommitment() public { + FlowMemoryHookAdapter adapter = new FlowMemoryHookAdapter(); + + vm.expectRevert(FlowMemoryHookAdapter.ZeroCommitment.selector); + adapter.afterSwap(address(this), keccak256("pool.alpha"), keccak256("rootfield.alpha"), bytes32(0), ""); + } + + function _assertTrue(bool condition) private pure { + if (!condition) revert AssertionFailed(); + } +} diff --git a/tests/README.md b/tests/README.md index e6da77c6..e15061db 100644 --- a/tests/README.md +++ b/tests/README.md @@ -5,7 +5,14 @@ The initial contract tests use Foundry without `forge-std` so they can run befor Run from the repository root: ```powershell -forge test --root . --contracts . --match-path tests/RootfieldRegistry.t.sol --out E:\tmp\flowmemory-forge-out --cache-path E:\tmp\flowmemory-forge-cache -vv +forge test ``` -When root-level config is in scope, add a `foundry.toml` that sets `src = "contracts"` and `test = "tests"` so the command can become `forge test`. +`foundry.toml` sets `contracts/` as the source directory and `tests/` as the test directory. Build output goes to `out/` and cache data goes to `cache/`, which should remain generated artifacts. + +Run a specific suite when iterating: + +```powershell +forge test --match-contract RootfieldRegistryTest +forge test --match-contract LiveV0PackageTest +``` diff --git a/tests/RootfieldRegistry.t.sol b/tests/RootfieldRegistry.t.sol index 45517caa..746c7d6a 100644 --- a/tests/RootfieldRegistry.t.sol +++ b/tests/RootfieldRegistry.t.sol @@ -12,32 +12,36 @@ interface Vm { function recordLogs() external; function getRecordedLogs() external returns (Log[] memory); + function expectRevert(bytes4 revertData) external; + function expectRevert(bytes calldata revertData) external; } contract RootfieldRegistryCaller { function submitRoot(RootfieldRegistry registry, bytes32 rootfieldId, bytes32 root) external { - registry.submitRoot(rootfieldId, root, bytes32("artifact"), bytes32(0), ""); + registry.submitRoot(rootfieldId, root, keccak256("artifact"), bytes32(0), ""); + } + + function deactivateRootfield(RootfieldRegistry registry, bytes32 rootfieldId) external { + registry.deactivateRootfield(rootfieldId, bytes32(0), "rootfield://deactivate"); } } contract RootfieldRegistryTest { Vm private constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); - bytes32 private constant FLOWPULSE_SIGNATURE = keccak256( - "FlowPulse(bytes32,bytes32,address,uint8,bytes32,bytes32,bytes32,uint64,uint64,string)" - ); + bytes32 private constant FLOWPULSE_SIGNATURE = + keccak256("FlowPulse(bytes32,bytes32,address,uint8,bytes32,bytes32,bytes32,uint64,uint64,string)"); RootfieldRegistry private registry; error AssertionFailed(); - error ExpectedRevert(); function setUp() public { registry = new RootfieldRegistry(); } function testRegisterRootfieldStoresCommitmentMetadata() public { - bytes32 rootfieldId = bytes32("rootfield.alpha"); - bytes32 schemaHash = bytes32("schema.v0"); + bytes32 rootfieldId = keccak256("rootfield.alpha"); + bytes32 schemaHash = keccak256("schema.v0"); bytes32 metadataHash = keccak256("metadata"); registry.registerRootfield(rootfieldId, schemaHash, metadataHash, "ipfs://metadata"); @@ -54,8 +58,8 @@ contract RootfieldRegistryTest { } function testRegisterRootfieldEmitsFlowPulseSchema() public { - bytes32 rootfieldId = bytes32("rootfield.beta"); - bytes32 schemaHash = bytes32("schema.v0"); + bytes32 rootfieldId = keccak256("rootfield.beta"); + bytes32 schemaHash = keccak256("schema.v0"); bytes32 metadataHash = keccak256("metadata"); vm.recordLogs(); @@ -89,27 +93,102 @@ contract RootfieldRegistryTest { _assertTrue(keccak256(bytes(uri)) == keccak256("ipfs://metadata")); } + function testRegistrationUriIsArbitraryAdvisoryLogData() public { + bytes32 rootfieldId = keccak256("rootfield.uri"); + string memory arbitraryURI = "not-a-short-pointer:raw caller supplied advisory string"; + + vm.recordLogs(); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), arbitraryURI); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + ( + uint8 pulseType, + bytes32 subject, + bytes32 commitment, + bytes32 parentPulseId, + uint64 sequence, + uint64 occurredAt, + string memory uri + ) = abi.decode(logs[0].data, (uint8, bytes32, bytes32, bytes32, uint64, uint64, string)); + + _assertTrue(pulseType == 1); + _assertTrue(subject == rootfieldId); + _assertTrue(commitment == keccak256(abi.encode(keccak256("schema.v0"), keccak256("metadata")))); + _assertTrue(parentPulseId == bytes32(0)); + _assertTrue(sequence == 1); + _assertTrue(occurredAt > 0); + _assertTrue(keccak256(bytes(uri)) == keccak256(bytes(arbitraryURI))); + } + + function testSubmitRootEmitsFlowPulseSchemaWithoutReceiptMetadata() public { + bytes32 rootfieldId = keccak256("rootfield.submit"); + bytes32 schemaHash = keccak256("schema.v0"); + bytes32 metadataHash = keccak256("metadata"); + bytes32 root = keccak256("root"); + bytes32 artifactCommitment = keccak256("artifact"); + bytes32 registrationPulseId = + registry.registerRootfield(rootfieldId, schemaHash, metadataHash, "ipfs://metadata"); + + vm.recordLogs(); + bytes32 pulseId = + registry.submitRoot(rootfieldId, root, artifactCommitment, registrationPulseId, "ipfs://evidence"); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + _assertTrue(logs.length == 1); + _assertTrue(logs[0].emitter == address(registry)); + _assertTrue(logs[0].topics.length == 4); + _assertTrue(logs[0].topics[0] == FLOWPULSE_SIGNATURE); + _assertTrue(logs[0].topics[1] == pulseId); + _assertTrue(logs[0].topics[2] == rootfieldId); + _assertTrue(logs[0].topics[3] == bytes32(uint256(uint160(address(this))))); + + ( + uint8 pulseType, + bytes32 subject, + bytes32 commitment, + bytes32 parentPulseId, + uint64 sequence, + uint64 occurredAt, + string memory uri + ) = abi.decode(logs[0].data, (uint8, bytes32, bytes32, bytes32, uint64, uint64, string)); + + _assertTrue(pulseType == 2); + _assertTrue(subject == root); + _assertTrue(commitment == keccak256(abi.encode(root, artifactCommitment))); + _assertTrue(parentPulseId == registrationPulseId); + _assertTrue(sequence == 2); + _assertTrue(occurredAt > 0); + _assertTrue(keccak256(bytes(uri)) == keccak256("ipfs://evidence")); + } + + function testFlowPulseSignatureExcludesReceiptMetadata() public pure { + bytes32 signatureWithReceiptMetadata = keccak256( + "FlowPulse(bytes32,bytes32,address,uint8,bytes32,bytes32,bytes32,uint64,uint64,string,bytes32,uint256)" + ); + + _assertTrue(FLOWPULSE_SIGNATURE != signatureWithReceiptMetadata); + } + function testCannotRegisterZeroRootfieldId() public { - try registry.registerRootfield(bytes32(0), bytes32("schema.v0"), bytes32("metadata"), "") { - revert ExpectedRevert(); - } catch {} + vm.expectRevert(RootfieldRegistry.ZeroRootfieldId.selector); + registry.registerRootfield(bytes32(0), keccak256("schema.v0"), keccak256("metadata"), ""); } function testCannotRegisterDuplicateRootfieldId() public { - bytes32 rootfieldId = bytes32("rootfield.gamma"); - registry.registerRootfield(rootfieldId, bytes32("schema.v0"), bytes32("metadata"), ""); + bytes32 rootfieldId = keccak256("rootfield.gamma"); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); - try registry.registerRootfield(rootfieldId, bytes32("schema.v1"), bytes32("metadata2"), "") { - revert ExpectedRevert(); - } catch {} + vm.expectRevert(abi.encodeWithSelector(RootfieldRegistry.RootfieldAlreadyRegistered.selector, rootfieldId)); + registry.registerRootfield(rootfieldId, keccak256("schema.v1"), keccak256("metadata2"), ""); } function testSubmitRootStoresLatestRootAndIncrementsCounts() public { - bytes32 rootfieldId = bytes32("rootfield.delta"); + bytes32 rootfieldId = keccak256("rootfield.delta"); bytes32 root = keccak256("root"); - bytes32 registrationPulseId = registry.registerRootfield(rootfieldId, bytes32("schema.v0"), bytes32("metadata"), ""); - registry.submitRoot(rootfieldId, root, bytes32("artifact"), registrationPulseId, "ipfs://evidence"); + bytes32 registrationPulseId = + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + registry.submitRoot(rootfieldId, root, keccak256("artifact"), registrationPulseId, "ipfs://evidence"); RootfieldRegistry.Rootfield memory rootfield = registry.getRootfield(rootfieldId); _assertTrue(rootfield.latestRoot == root); @@ -117,14 +196,112 @@ contract RootfieldRegistryTest { _assertTrue(rootfield.rootCount == 1); } + function testCannotSubmitZeroRoot() public { + bytes32 rootfieldId = keccak256("rootfield.zero-root"); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + vm.expectRevert(RootfieldRegistry.ZeroRoot.selector); + registry.submitRoot(rootfieldId, bytes32(0), keccak256("artifact"), bytes32(0), ""); + } + + function testCannotSubmitUnregisteredRootfield() public { + bytes32 rootfieldId = keccak256("rootfield.missing"); + + vm.expectRevert(abi.encodeWithSelector(RootfieldRegistry.RootfieldNotRegistered.selector, rootfieldId)); + registry.submitRoot(rootfieldId, keccak256("root"), keccak256("artifact"), bytes32(0), ""); + } + function testOnlyRootfieldOwnerCanSubmitRoot() public { - bytes32 rootfieldId = bytes32("rootfield.epsilon"); - registry.registerRootfield(rootfieldId, bytes32("schema.v0"), bytes32("metadata"), ""); + bytes32 rootfieldId = keccak256("rootfield.epsilon"); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + RootfieldRegistryCaller caller = new RootfieldRegistryCaller(); + vm.expectRevert( + abi.encodeWithSelector(RootfieldRegistry.NotRootfieldOwner.selector, rootfieldId, address(caller)) + ); + caller.submitRoot(registry, rootfieldId, keccak256("root")); + } + + function testDeactivateRootfieldEmitsStatusPulseAndBlocksRoots() public { + bytes32 rootfieldId = keccak256("rootfield.deactivate"); + bytes32 registrationPulseId = + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + vm.recordLogs(); + bytes32 pulseId = registry.deactivateRootfield(rootfieldId, registrationPulseId, "rootfield://deactivate"); + Vm.Log[] memory logs = vm.getRecordedLogs(); + + _assertTrue(logs.length == 2); + _assertTrue(logs[0].emitter == address(registry)); + _assertTrue(logs[0].topics[0] == FLOWPULSE_SIGNATURE); + _assertTrue(logs[0].topics[1] == pulseId); + + ( + uint8 pulseType, + bytes32 subject, + bytes32 commitment, + bytes32 parentPulseId, + uint64 sequence, + uint64 occurredAt, + string memory uri + ) = abi.decode(logs[0].data, (uint8, bytes32, bytes32, bytes32, uint64, uint64, string)); + + _assertTrue(pulseType == 3); + _assertTrue(subject == rootfieldId); + _assertTrue(commitment == keccak256(abi.encode(rootfieldId, false))); + _assertTrue(parentPulseId == registrationPulseId); + _assertTrue(sequence == 2); + _assertTrue(occurredAt > 0); + _assertTrue(keccak256(bytes(uri)) == keccak256("rootfield://deactivate")); + + RootfieldRegistry.Rootfield memory rootfield = registry.getRootfield(rootfieldId); + _assertTrue(!rootfield.active); + _assertTrue(rootfield.pulseCount == 2); + + vm.expectRevert(abi.encodeWithSelector(RootfieldRegistry.RootfieldInactive.selector, rootfieldId)); + registry.submitRoot(rootfieldId, keccak256("root"), keccak256("artifact"), pulseId, ""); + } + + function testOnlyRootfieldOwnerCanDeactivateRootfield() public { + bytes32 rootfieldId = keccak256("rootfield.deactivate.owner"); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); RootfieldRegistryCaller caller = new RootfieldRegistryCaller(); - try caller.submitRoot(registry, rootfieldId, keccak256("root")) { - revert ExpectedRevert(); - } catch {} + vm.expectRevert( + abi.encodeWithSelector(RootfieldRegistry.NotRootfieldOwner.selector, rootfieldId, address(caller)) + ); + caller.deactivateRootfield(registry, rootfieldId); + } + + function testTransferRootfieldOwnershipAllowsNewOwnerOnly() public { + bytes32 rootfieldId = keccak256("rootfield.transfer"); + RootfieldRegistryCaller newOwner = new RootfieldRegistryCaller(); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + registry.transferRootfieldOwnership(rootfieldId, address(newOwner), "rootfield://transfer"); + + RootfieldRegistry.Rootfield memory rootfield = registry.getRootfield(rootfieldId); + _assertTrue(rootfield.owner == address(newOwner)); + _assertTrue(rootfield.pulseCount == 2); + + vm.expectRevert( + abi.encodeWithSelector(RootfieldRegistry.NotRootfieldOwner.selector, rootfieldId, address(this)) + ); + registry.submitRoot(rootfieldId, keccak256("root.old-owner"), keccak256("artifact"), bytes32(0), ""); + + newOwner.submitRoot(registry, rootfieldId, keccak256("root.new-owner")); + rootfield = registry.getRootfield(rootfieldId); + _assertTrue(rootfield.latestRoot == keccak256("root.new-owner")); + _assertTrue(rootfield.rootCount == 1); + _assertTrue(rootfield.pulseCount == 3); + } + + function testCannotTransferRootfieldToZeroOwner() public { + bytes32 rootfieldId = keccak256("rootfield.transfer.zero"); + registry.registerRootfield(rootfieldId, keccak256("schema.v0"), keccak256("metadata"), ""); + + vm.expectRevert(RootfieldRegistry.ZeroRootfieldOwner.selector); + registry.transferRootfieldOwnership(rootfieldId, address(0), ""); } function _assertTrue(bool condition) private pure {