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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions contracts/ArtifactRegistry.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
80 changes: 80 additions & 0 deletions contracts/CursorRegistry.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
4 changes: 2 additions & 2 deletions contracts/FLOWPULSE_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions contracts/FlowMemoryHookAdapter.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion contracts/FlowPulse.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions contracts/ReceiptVerifier.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
65 changes: 64 additions & 1 deletion contracts/RootfieldRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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];
}
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading