// SPDX-License-Identifier: AGPL-3.0-only pragma solidity ^0.8.24; import {EveryChannelWitnessRegistry} from "./EveryChannelWitnessRegistry.sol"; /// @title EveryChannelObservationLedger /// @notice Observation consensus ledger for reality-derived every.channel epochs. /// Witnesses attest to the same derived observation hash, and the first candidate to hit quorum /// finalizes the `(stream, epoch)` slot. contract EveryChannelObservationLedger { struct ObservationHeader { bytes32 streamHash; bytes32 epochHash; bytes32 parentObservationHash; bytes32 dataRoot; bytes32 locatorHash; uint64 observedUnixMs; uint64 sequence; } struct Candidate { bytes32 slot; address proposer; uint64 observedUnixMs; uint64 sequence; uint32 attestations; bool finalized; } EveryChannelWitnessRegistry public immutable witnessRegistry; uint256 public immutable quorum; mapping(bytes32 => ObservationHeader) private observationHeaders; mapping(bytes32 => Candidate) public candidates; mapping(bytes32 => bytes32) public finalizedObservationBySlot; mapping(bytes32 => mapping(address => bool)) public hasAttested; event ObservationProposed( bytes32 indexed slot, bytes32 indexed observationHash, address indexed proposer, bytes32 streamHash, bytes32 epochHash, bytes32 parentObservationHash, bytes32 dataRoot, bytes32 locatorHash, uint64 observedUnixMs, uint64 sequence ); event ObservationAttested( bytes32 indexed slot, bytes32 indexed observationHash, address indexed witness, uint32 attestations ); event ObservationFinalized( bytes32 indexed slot, bytes32 indexed observationHash, uint32 attestations ); error NotWitness(address caller); error InvalidRegistry(address registry); error InvalidQuorum(uint256 quorum); error UnknownObservation(bytes32 observationHash); error ObservationAlreadyExists(bytes32 observationHash); error ObservationSlotAlreadyFinalized(bytes32 slot, bytes32 observationHash); error DuplicateAttestation(bytes32 observationHash, address witness); constructor(address registry, uint256 quorumThreshold) { if (registry == address(0)) revert InvalidRegistry(registry); if (quorumThreshold == 0) revert InvalidQuorum(quorumThreshold); witnessRegistry = EveryChannelWitnessRegistry(registry); quorum = quorumThreshold; } modifier onlyWitness() { if (!witnessRegistry.isWitness(msg.sender)) revert NotWitness(msg.sender); _; } function observationSlot( bytes32 streamHash, bytes32 epochHash ) public pure returns (bytes32) { return keccak256(abi.encode(streamHash, epochHash)); } function hashObservationHeader( ObservationHeader memory header ) public pure returns (bytes32) { return keccak256( abi.encode( header.streamHash, header.epochHash, header.parentObservationHash, header.dataRoot, header.locatorHash, header.observedUnixMs, header.sequence ) ); } function getObservationHeader( bytes32 observationHash ) external view returns (ObservationHeader memory) { if (candidates[observationHash].slot == bytes32(0)) { revert UnknownObservation(observationHash); } return observationHeaders[observationHash]; } function proposeObservation( ObservationHeader calldata header ) external returns (bytes32 observationHash) { observationHash = hashObservationHeader(header); bytes32 slot = observationSlot(header.streamHash, header.epochHash); bytes32 finalized = finalizedObservationBySlot[slot]; if (finalized != bytes32(0) && finalized != observationHash) { revert ObservationSlotAlreadyFinalized(slot, finalized); } if (candidates[observationHash].slot != bytes32(0)) { revert ObservationAlreadyExists(observationHash); } observationHeaders[observationHash] = header; candidates[observationHash] = Candidate({ slot: slot, proposer: msg.sender, observedUnixMs: header.observedUnixMs, sequence: header.sequence, attestations: 0, finalized: false }); emit ObservationProposed( slot, observationHash, msg.sender, header.streamHash, header.epochHash, header.parentObservationHash, header.dataRoot, header.locatorHash, header.observedUnixMs, header.sequence ); if (witnessRegistry.isWitness(msg.sender)) { _attest(observationHash, msg.sender); } } function attestObservation(bytes32 observationHash) external onlyWitness { _attest(observationHash, msg.sender); } function _attest(bytes32 observationHash, address witness) internal { Candidate storage candidate = candidates[observationHash]; if (candidate.slot == bytes32(0)) revert UnknownObservation(observationHash); if (hasAttested[observationHash][witness]) { revert DuplicateAttestation(observationHash, witness); } bytes32 finalized = finalizedObservationBySlot[candidate.slot]; if (finalized != bytes32(0) && finalized != observationHash) { revert ObservationSlotAlreadyFinalized(candidate.slot, finalized); } hasAttested[observationHash][witness] = true; candidate.attestations += 1; emit ObservationAttested( candidate.slot, observationHash, witness, candidate.attestations ); if (!candidate.finalized && candidate.attestations >= quorum) { candidate.finalized = true; finalizedObservationBySlot[candidate.slot] = observationHash; emit ObservationFinalized( candidate.slot, observationHash, candidate.attestations ); } } }