193 lines
6.3 KiB
Solidity
193 lines
6.3 KiB
Solidity
// 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
|
|
);
|
|
}
|
|
}
|
|
}
|