// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; contract ManifestHeartbeatChain { error AlreadyRegistered(); error NotFound(); error ExpiredEntry(); error NotExpired(); struct Entry { bytes32 head; uint256 lastBeat; } mapping(address => Entry) public entries; uint256 private constant TTL = 1 days; event Registered(address indexed agent, bytes32 manifestRoot, bytes32 head, uint256 timestamp); event Updated(address indexed agent, bytes32 manifestRoot, bytes32 newHead, uint256 timestamp); event Beat(address indexed agent, uint256 timestamp); event Expired(address indexed agent); function register(bytes32 manifestRoot) external { if (entries[msg.sender].lastBeat != 0) revert AlreadyRegistered(); bytes32 head = keccak256(abi.encodePacked(bytes32(0), manifestRoot, block.timestamp)); entries[msg.sender] = Entry(head, block.timestamp); emit Registered(msg.sender, manifestRoot, head, block.timestamp); } function update(bytes32 manifestRoot) external { Entry storage e = entries[msg.sender]; if (e.lastBeat == 0) revert NotFound(); uint256 ts = block.timestamp; if (ts > e.lastBeat + TTL) revert ExpiredEntry(); e.lastBeat = ts; e.head = keccak256(abi.encodePacked(e.head, manifestRoot, ts)); emit Updated(msg.sender, manifestRoot, e.head, ts); } function beat() external { Entry storage e = entries[msg.sender]; if (e.lastBeat == 0) revert NotFound(); if (block.timestamp > e.lastBeat + TTL) revert ExpiredEntry(); e.lastBeat = block.timestamp; emit Beat(msg.sender, block.timestamp); } function expire(address agent) external { Entry storage e = entries[agent]; if (e.lastBeat == 0) revert NotFound(); if (block.timestamp <= e.lastBeat + TTL) revert NotExpired(); delete entries[agent]; emit Expired(agent); } }