// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract AuditReceiptLedger { struct AuditReceipt { bytes16 gatewayUuid; bytes32 merkleRoot; uint256 objectCount; uint256 timestamp; uint256 nonce; } bytes32 public immutable DOMAIN_SEPARATOR; bytes32 public constant AUDIT_RECEIPT_TYPEHASH = keccak256( "AuditReceipt(bytes16 gatewayUuid,bytes32 merkleRoot,uint256 objectCount,uint256 timestamp,uint256 nonce)" ); address public immutable authorizedSigner; mapping(uint256 => bool) public usedNonces; mapping(bytes32 => bool) public verifiedMerkleRoots; event ReceiptAnchored(bytes16 indexed gatewayUuid, bytes32 indexed merkleRoot, uint256 objectCount, uint256 timestamp); constructor(address _authorizedSigner) { authorizedSigner = _authorizedSigner; DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256(bytes("AuditReceiptLedger")), keccak256(bytes("1")), block.chainid, address(this) ) ); } function verifyAndAnchor(AuditReceipt calldata receipt, bytes calldata signature) external returns (bool) { require(!usedNonces[receipt.nonce], "Nonce used"); require(receipt.timestamp + 300 >= block.timestamp && receipt.timestamp <= block.timestamp + 300, "Timestamp drift"); bytes32 structHash = keccak256( abi.encode(AUDIT_RECEIPT_TYPEHASH, receipt.gatewayUuid, receipt.merkleRoot, receipt.objectCount, receipt.timestamp, receipt.nonce) ); bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash)); address signer = recoverSigner(digest, signature); require(signer == authorizedSigner, "Invalid signer"); usedNonces[receipt.nonce] = true; verifiedMerkleRoots[receipt.merkleRoot] = true; emit ReceiptAnchored(receipt.gatewayUuid, receipt.merkleRoot, receipt.objectCount, receipt.timestamp); return true; } function recoverSigner(bytes32 digest, bytes memory sig) internal pure returns (address) { require(sig.length == 65, "Sig length"); bytes32 r; bytes32 s; uint8 v; assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) v := byte(0, mload(add(sig, 96))) } if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E735E5C9258C5716C851F818E00) revert("Invalid s"); if (v != 27 && v != 28) revert("Invalid v"); address signer = ecrecover(digest, v, r, s); require(signer != address(0), "Zero signer"); return signer; } }