// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { EIP712AuthV3 } from "./common/EIP712AuthV3.sol"; /// @notice Portable provenance receipts. Registration asserts signature validity, not payload truth. contract PortableReceiptsV3 is EIP712AuthV3 { struct Receipt { address issuer; bytes32 subject; bytes32 schema; bytes32 payloadHash; bytes32 provenanceHash; bytes32 consentId; uint64 issuedAt; uint64 notAfter; uint256 nonce; } bytes32 public constant RECEIPT_TYPEHASH = keccak256( "Receipt(address issuer,bytes32 subject,bytes32 schema,bytes32 payloadHash,bytes32 provenanceHash,bytes32 consentId,uint64 issuedAt,uint64 notAfter,uint256 nonce)" ); mapping(address => uint256) public nextNonce; mapping(bytes32 => bytes32) public receiptDigest; error BadReceipt(); error BadNonce(); error DuplicateReceipt(); error Unauthorized(); event ReceiptRegisteredV3( bytes32 indexed receiptId, address indexed issuer, bytes32 indexed subject, bytes32 schema, bytes32 payloadHash, bytes32 provenanceHash, bytes32 consentId ); constructor() EIP712AuthV3("Vaultfire Portable Receipts", "3") { } function receiptId(Receipt calldata receipt) public pure returns (bytes32) { return keccak256(abi.encode(receipt)); } function registerDirect(Receipt calldata receipt) external returns (bytes32 id) { if (msg.sender != receipt.issuer) revert Unauthorized(); id = _register(receipt); } function registerBySignature(Receipt calldata receipt, bytes calldata signature) external returns (bytes32 id) { bytes32 structHash = _structHash(receipt); _requireSignature(receipt.issuer, _hashTypedData(structHash), signature); id = _register(receipt); } function _register(Receipt calldata receipt) private returns (bytes32 id) { if ( receipt.issuer == address(0) || receipt.subject == bytes32(0) || receipt.schema == bytes32(0) || receipt.payloadHash == bytes32(0) || receipt.provenanceHash == bytes32(0) || receipt.issuedAt > block.timestamp || receipt.notAfter <= block.timestamp || receipt.issuedAt > receipt.notAfter ) revert BadReceipt(); if (receipt.nonce != nextNonce[receipt.issuer]) revert BadNonce(); id = receiptId(receipt); if (receiptDigest[id] != bytes32(0)) revert DuplicateReceipt(); nextNonce[receipt.issuer] = receipt.nonce + 1; bytes32 digest = _hashTypedData(_structHash(receipt)); receiptDigest[id] = digest; emit ReceiptRegisteredV3( id, receipt.issuer, receipt.subject, receipt.schema, receipt.payloadHash, receipt.provenanceHash, receipt.consentId ); } function _structHash(Receipt calldata receipt) private pure returns (bytes32) { return keccak256( abi.encode( RECEIPT_TYPEHASH, receipt.issuer, receipt.subject, receipt.schema, receipt.payloadHash, receipt.provenanceHash, receipt.consentId, receipt.issuedAt, receipt.notAfter, receipt.nonce ) ); } }