// SPDX-License-Identifier: MIT pragma solidity 0.8.30; /// @title TizmoRegistry /// @notice An append-only record of hashes submitted by a wallet, never a token. /// @dev A record is evidence of submission, not proof of authorship or ownership. contract TizmoRegistry { uint256 public constant MAX_METADATA_URI_BYTES = 512; struct Record { bytes32 fileHash; bytes32 recipeHash; address registrant; uint64 timestamp; bytes32 previousId; string metadataURI; } mapping(bytes32 => Record) private records; mapping(address => mapping(bytes32 => bytes32)) public recordFor; error ZeroHash(); error DuplicateRecord(bytes32 existingId); error UnknownRecord(bytes32 recordId); error DifferentRegistrant(bytes32 previousId); error MetadataTooLong(uint256 byteLength); event WorkRegistered( bytes32 indexed recordId, address indexed registrant, bytes32 indexed fileHash, bytes32 recipeHash, bytes32 previousId, uint64 timestamp, string metadataURI ); /// @param fileHash SHA256 digest of the exact exported file bytes. /// @param recipeHash SHA256 digest of the canonical renderer recipe bytes. /// @param previousId Earlier record by this wallet, or zero for a first version. /// @param metadataURI Optional public metadata reference, limited to 512 UTF8 bytes. function register( bytes32 fileHash, bytes32 recipeHash, bytes32 previousId, string calldata metadataURI ) external returns (bytes32 recordId) { if (fileHash == bytes32(0) || recipeHash == bytes32(0)) revert ZeroHash(); bytes32 existingId = recordFor[msg.sender][fileHash]; if (existingId != bytes32(0)) revert DuplicateRecord(existingId); uint256 uriLength = bytes(metadataURI).length; if (uriLength > MAX_METADATA_URI_BYTES) revert MetadataTooLong(uriLength); if (previousId != bytes32(0)) { address previousRegistrant = records[previousId].registrant; if (previousRegistrant == address(0)) revert UnknownRecord(previousId); if (previousRegistrant != msg.sender) revert DifferentRegistrant(previousId); } recordId = keccak256(abi.encode(msg.sender, fileHash, block.chainid, address(this))); uint64 submittedAt = uint64(block.timestamp); records[recordId] = Record({ fileHash: fileHash, recipeHash: recipeHash, registrant: msg.sender, timestamp: submittedAt, previousId: previousId, metadataURI: metadataURI }); recordFor[msg.sender][fileHash] = recordId; emit WorkRegistered(recordId, msg.sender, fileHash, recipeHash, previousId, submittedAt, metadataURI); } function getRecord(bytes32 recordId) external view returns (Record memory record) { record = records[recordId]; if (record.registrant == address(0)) revert UnknownRecord(recordId); } }