// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/Ownable.sol"; /** * @title DocumentChain * @notice Records and verifies document hashes on the blockchain * @dev Provides tamper-proof document verification */ contract DocumentChain is Ownable { struct DocumentRecord { bytes32 id; string requestId; string documentId; string hash; uint256 version; uint256 timestamp; address uploadedBy; } // Mapping from document ID to its records mapping(string => DocumentRecord[]) private _documentHistory; // Mapping from document ID to latest hash mapping(string => string) private _latestHashes; // Mapping from hash to document ID (for reverse lookup) mapping(string => string) private _hashToDocument; // Counter for unique record IDs uint256 private _recordCounter; // Events event DocumentRecorded( bytes32 indexed recordId, string indexed requestId, string documentId, string hash, uint256 version, uint256 timestamp ); constructor() Ownable(msg.sender) {} /** * @notice Record a document hash * @param requestId The license request ID * @param documentId The document ID * @param hash The document hash (e.g., SHA-256) * @param version The document version * @return recordId The unique record ID */ function recordDocumentHash( string calldata requestId, string calldata documentId, string calldata hash, uint256 version ) public onlyOwner returns (bytes32) { require(bytes(requestId).length > 0, "Request ID required"); require(bytes(documentId).length > 0, "Document ID required"); require(bytes(hash).length > 0, "Hash required"); _recordCounter++; bytes32 recordId = keccak256( abi.encodePacked(documentId, version, block.timestamp, _recordCounter) ); DocumentRecord memory record = DocumentRecord({ id: recordId, requestId: requestId, documentId: documentId, hash: hash, version: version, timestamp: block.timestamp, uploadedBy: msg.sender }); _documentHistory[documentId].push(record); _latestHashes[documentId] = hash; _hashToDocument[hash] = documentId; emit DocumentRecorded( recordId, requestId, documentId, hash, version, block.timestamp ); return recordId; } /** * @notice Verify if a document hash exists * @param documentId The document ID * @param hash The hash to verify * @return True if the hash matches any recorded version */ function verifyDocumentHash(string calldata documentId, string calldata hash) public view returns (bool) { DocumentRecord[] memory history = _documentHistory[documentId]; for (uint256 i = 0; i < history.length; i++) { if (keccak256(bytes(history[i].hash)) == keccak256(bytes(hash))) { return true; } } return false; } /** * @notice Verify if a hash is the latest version * @param documentId The document ID * @param hash The hash to verify * @return True if the hash is the latest version */ function verifyLatestHash(string calldata documentId, string calldata hash) public view returns (bool) { return keccak256(bytes(_latestHashes[documentId])) == keccak256(bytes(hash)); } /** * @notice Get the complete history of a document * @param documentId The document ID * @return Array of DocumentRecord structs */ function getDocumentHistory(string calldata documentId) public view returns (DocumentRecord[] memory) { return _documentHistory[documentId]; } /** * @notice Get the latest hash for a document * @param documentId The document ID * @return The latest document hash */ function getLatestDocumentHash(string calldata documentId) public view returns (string memory) { return _latestHashes[documentId]; } /** * @notice Get document ID by hash * @param hash The document hash * @return The document ID */ function getDocumentByHash(string calldata hash) public view returns (string memory) { return _hashToDocument[hash]; } /** * @notice Get the version count for a document * @param documentId The document ID * @return The number of versions */ function getVersionCount(string calldata documentId) public view returns (uint256) { return _documentHistory[documentId].length; } /** * @notice Get a specific version of a document * @param documentId The document ID * @param version The version number (1-indexed) * @return The DocumentRecord for that version */ function getDocumentVersion(string calldata documentId, uint256 version) public view returns (DocumentRecord memory) { require(version > 0 && version <= _documentHistory[documentId].length, "Invalid version"); return _documentHistory[documentId][version - 1]; } }