Advance forge rollout, Ethereum rails, and NBC sources
This commit is contained in:
parent
be26313225
commit
7d84510eac
88 changed files with 11230 additions and 302 deletions
193
contracts/EveryChannelObservationLedger.sol
Normal file
193
contracts/EveryChannelObservationLedger.sol
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
106
contracts/EveryChannelRail.sol
Normal file
106
contracts/EveryChannelRail.sol
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// @title EveryChannelRail
|
||||
/// @notice Storage-light Ethereum rails for every.channel stream identity, manifest, and
|
||||
/// transport attestations. The intended deployment target is a private Ethereum-compatible
|
||||
/// network where iroh remains the transport/control plane and this contract carries compact
|
||||
/// settlement metadata only.
|
||||
contract EveryChannelRail {
|
||||
struct ManifestPointer {
|
||||
bytes32 streamIdHash;
|
||||
bytes32 epochIdHash;
|
||||
bytes32 manifestId;
|
||||
bytes32 bodyCommitment;
|
||||
bytes32 dataRoot;
|
||||
uint64 createdUnixMs;
|
||||
address announcer;
|
||||
}
|
||||
|
||||
struct AnnouncementPointer {
|
||||
bytes32 streamIdHash;
|
||||
bytes32 announcementCommitment;
|
||||
uint64 updatedUnixMs;
|
||||
uint64 ttlMs;
|
||||
address announcer;
|
||||
}
|
||||
|
||||
mapping(bytes32 => ManifestPointer) public latestManifestByEpoch;
|
||||
mapping(bytes32 => AnnouncementPointer) public latestAnnouncementByStream;
|
||||
|
||||
event ManifestCommitted(
|
||||
bytes32 indexed streamIdHash,
|
||||
bytes32 indexed epochIdHash,
|
||||
bytes32 indexed manifestId,
|
||||
bytes32 bodyCommitment,
|
||||
bytes32 dataRoot,
|
||||
uint64 createdUnixMs,
|
||||
address announcer
|
||||
);
|
||||
|
||||
event TransportAnnounced(
|
||||
bytes32 indexed streamIdHash,
|
||||
bytes32 indexed announcementCommitment,
|
||||
uint64 updatedUnixMs,
|
||||
uint64 ttlMs,
|
||||
address announcer
|
||||
);
|
||||
|
||||
function commitManifest(
|
||||
string calldata streamId,
|
||||
string calldata epochId,
|
||||
bytes32 manifestId,
|
||||
bytes32 bodyCommitment,
|
||||
bytes32 dataRoot,
|
||||
uint64 createdUnixMs
|
||||
) external {
|
||||
bytes32 streamIdHash = keccak256(bytes(streamId));
|
||||
bytes32 epochIdHash = keccak256(bytes(epochId));
|
||||
bytes32 slot = keccak256(abi.encode(streamIdHash, epochIdHash));
|
||||
|
||||
latestManifestByEpoch[slot] = ManifestPointer({
|
||||
streamIdHash: streamIdHash,
|
||||
epochIdHash: epochIdHash,
|
||||
manifestId: manifestId,
|
||||
bodyCommitment: bodyCommitment,
|
||||
dataRoot: dataRoot,
|
||||
createdUnixMs: createdUnixMs,
|
||||
announcer: msg.sender
|
||||
});
|
||||
|
||||
emit ManifestCommitted(
|
||||
streamIdHash,
|
||||
epochIdHash,
|
||||
manifestId,
|
||||
bodyCommitment,
|
||||
dataRoot,
|
||||
createdUnixMs,
|
||||
msg.sender
|
||||
);
|
||||
}
|
||||
|
||||
function announceTransport(
|
||||
string calldata streamId,
|
||||
bytes32 announcementCommitment,
|
||||
uint64 updatedUnixMs,
|
||||
uint64 ttlMs
|
||||
) external {
|
||||
bytes32 streamIdHash = keccak256(bytes(streamId));
|
||||
|
||||
latestAnnouncementByStream[streamIdHash] = AnnouncementPointer({
|
||||
streamIdHash: streamIdHash,
|
||||
announcementCommitment: announcementCommitment,
|
||||
updatedUnixMs: updatedUnixMs,
|
||||
ttlMs: ttlMs,
|
||||
announcer: msg.sender
|
||||
});
|
||||
|
||||
emit TransportAnnounced(
|
||||
streamIdHash,
|
||||
announcementCommitment,
|
||||
updatedUnixMs,
|
||||
ttlMs,
|
||||
msg.sender
|
||||
);
|
||||
}
|
||||
}
|
||||
53
contracts/EveryChannelWitnessRegistry.sol
Normal file
53
contracts/EveryChannelWitnessRegistry.sol
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
/// @title EveryChannelWitnessRegistry
|
||||
/// @notice Minimal registry-backed witness set for observation consensus. This is intentionally
|
||||
/// simple for the first testnet tranche: explicit membership, no staking, no slashing.
|
||||
contract EveryChannelWitnessRegistry {
|
||||
address public owner;
|
||||
mapping(address => bool) public isWitness;
|
||||
uint256 public witnessCount;
|
||||
|
||||
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
|
||||
event WitnessAdded(address indexed witness);
|
||||
event WitnessRemoved(address indexed witness);
|
||||
|
||||
error NotOwner();
|
||||
error ZeroAddress();
|
||||
error WitnessAlreadyRegistered(address witness);
|
||||
error WitnessNotRegistered(address witness);
|
||||
|
||||
constructor(address initialOwner) {
|
||||
if (initialOwner == address(0)) revert ZeroAddress();
|
||||
owner = initialOwner;
|
||||
emit OwnershipTransferred(address(0), initialOwner);
|
||||
}
|
||||
|
||||
modifier onlyOwner() {
|
||||
if (msg.sender != owner) revert NotOwner();
|
||||
_;
|
||||
}
|
||||
|
||||
function transferOwnership(address newOwner) external onlyOwner {
|
||||
if (newOwner == address(0)) revert ZeroAddress();
|
||||
address previous = owner;
|
||||
owner = newOwner;
|
||||
emit OwnershipTransferred(previous, newOwner);
|
||||
}
|
||||
|
||||
function addWitness(address witness) external onlyOwner {
|
||||
if (witness == address(0)) revert ZeroAddress();
|
||||
if (isWitness[witness]) revert WitnessAlreadyRegistered(witness);
|
||||
isWitness[witness] = true;
|
||||
witnessCount += 1;
|
||||
emit WitnessAdded(witness);
|
||||
}
|
||||
|
||||
function removeWitness(address witness) external onlyOwner {
|
||||
if (!isWitness[witness]) revert WitnessNotRegistered(witness);
|
||||
isWitness[witness] = false;
|
||||
witnessCount -= 1;
|
||||
emit WitnessRemoved(witness);
|
||||
}
|
||||
}
|
||||
158
contracts/test/ObservationLedger.t.sol
Normal file
158
contracts/test/ObservationLedger.t.sol
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
pragma solidity ^0.8.24;
|
||||
|
||||
import {EveryChannelObservationLedger} from "../EveryChannelObservationLedger.sol";
|
||||
import {EveryChannelWitnessRegistry} from "../EveryChannelWitnessRegistry.sol";
|
||||
|
||||
contract WitnessActor {
|
||||
function propose(
|
||||
EveryChannelObservationLedger ledger,
|
||||
EveryChannelObservationLedger.ObservationHeader calldata header
|
||||
) external returns (bytes32) {
|
||||
return ledger.proposeObservation(header);
|
||||
}
|
||||
|
||||
function attest(
|
||||
EveryChannelObservationLedger ledger,
|
||||
bytes32 observationHash
|
||||
) external {
|
||||
ledger.attestObservation(observationHash);
|
||||
}
|
||||
}
|
||||
|
||||
contract ObservationLedgerTest {
|
||||
function _header(
|
||||
bytes32 streamHash,
|
||||
bytes32 epochHash,
|
||||
uint64 sequence,
|
||||
bytes32 dataRoot
|
||||
) internal pure returns (EveryChannelObservationLedger.ObservationHeader memory) {
|
||||
return EveryChannelObservationLedger.ObservationHeader({
|
||||
streamHash: streamHash,
|
||||
epochHash: epochHash,
|
||||
parentObservationHash: bytes32(0),
|
||||
dataRoot: dataRoot,
|
||||
locatorHash: keccak256(abi.encodePacked(streamHash, epochHash, sequence)),
|
||||
observedUnixMs: 1_772_001_256_329,
|
||||
sequence: sequence
|
||||
});
|
||||
}
|
||||
|
||||
function test_finalizes_when_quorum_is_reached() public {
|
||||
EveryChannelWitnessRegistry registry = new EveryChannelWitnessRegistry(address(this));
|
||||
WitnessActor witnessA = new WitnessActor();
|
||||
WitnessActor witnessB = new WitnessActor();
|
||||
registry.addWitness(address(witnessA));
|
||||
registry.addWitness(address(witnessB));
|
||||
|
||||
EveryChannelObservationLedger ledger = new EveryChannelObservationLedger(
|
||||
address(registry),
|
||||
2
|
||||
);
|
||||
|
||||
EveryChannelObservationLedger.ObservationHeader memory header = _header(
|
||||
keccak256("la-cbs:video0.m4s"),
|
||||
keccak256("epoch-229"),
|
||||
229,
|
||||
keccak256("58cd13f693debd000d995c9a5574e8b9274cc4d3399eb6f1f22393af1ba7407d")
|
||||
);
|
||||
|
||||
bytes32 observationHash = witnessA.propose(ledger, header);
|
||||
bytes32 slot = ledger.observationSlot(header.streamHash, header.epochHash);
|
||||
|
||||
assert(ledger.finalizedObservationBySlot(slot) == bytes32(0));
|
||||
|
||||
witnessB.attest(ledger, observationHash);
|
||||
|
||||
assert(ledger.finalizedObservationBySlot(slot) == observationHash);
|
||||
EveryChannelObservationLedger.ObservationHeader memory storedHeader = ledger
|
||||
.getObservationHeader(observationHash);
|
||||
assert(storedHeader.streamHash == header.streamHash);
|
||||
assert(storedHeader.epochHash == header.epochHash);
|
||||
assert(storedHeader.dataRoot == header.dataRoot);
|
||||
assert(storedHeader.sequence == header.sequence);
|
||||
}
|
||||
|
||||
function test_rejects_duplicate_attestation() public {
|
||||
EveryChannelWitnessRegistry registry = new EveryChannelWitnessRegistry(address(this));
|
||||
WitnessActor witnessA = new WitnessActor();
|
||||
registry.addWitness(address(witnessA));
|
||||
|
||||
EveryChannelObservationLedger ledger = new EveryChannelObservationLedger(
|
||||
address(registry),
|
||||
1
|
||||
);
|
||||
|
||||
EveryChannelObservationLedger.ObservationHeader memory header = _header(
|
||||
keccak256("la-nbc:catalog.json"),
|
||||
keccak256("epoch-5"),
|
||||
5,
|
||||
keccak256("f6ea0793fd00e29ced670d586e0e5f7f3d0f5edfc016c03f80710bd4bed587ec")
|
||||
);
|
||||
|
||||
bytes32 observationHash = witnessA.propose(ledger, header);
|
||||
assert(
|
||||
ledger.finalizedObservationBySlot(
|
||||
ledger.observationSlot(header.streamHash, header.epochHash)
|
||||
) == observationHash
|
||||
);
|
||||
|
||||
(bool ok, ) = address(witnessA).call(
|
||||
abi.encodeWithSelector(
|
||||
WitnessActor.attest.selector,
|
||||
ledger,
|
||||
observationHash
|
||||
)
|
||||
);
|
||||
assert(!ok);
|
||||
}
|
||||
|
||||
function test_competing_candidates_only_one_slot_can_finalize() public {
|
||||
EveryChannelWitnessRegistry registry = new EveryChannelWitnessRegistry(address(this));
|
||||
WitnessActor witnessA = new WitnessActor();
|
||||
WitnessActor witnessB = new WitnessActor();
|
||||
WitnessActor witnessC = new WitnessActor();
|
||||
registry.addWitness(address(witnessA));
|
||||
registry.addWitness(address(witnessB));
|
||||
registry.addWitness(address(witnessC));
|
||||
|
||||
EveryChannelObservationLedger ledger = new EveryChannelObservationLedger(
|
||||
address(registry),
|
||||
2
|
||||
);
|
||||
|
||||
bytes32 streamHash = keccak256("la-cbs:video0.m4s");
|
||||
bytes32 epochHash = keccak256("epoch-230");
|
||||
EveryChannelObservationLedger.ObservationHeader memory candidateA = _header(
|
||||
streamHash,
|
||||
epochHash,
|
||||
230,
|
||||
keccak256("1130c51b3ce428505dbc3c3294678f76e3200221d853fc9b02c8ed603ecc8b8c")
|
||||
);
|
||||
EveryChannelObservationLedger.ObservationHeader memory candidateB = _header(
|
||||
streamHash,
|
||||
epochHash,
|
||||
230,
|
||||
keccak256("deadbeef")
|
||||
);
|
||||
|
||||
bytes32 hashA = witnessA.propose(ledger, candidateA);
|
||||
bytes32 hashB = witnessB.propose(ledger, candidateB);
|
||||
bytes32 slot = ledger.observationSlot(streamHash, epochHash);
|
||||
|
||||
assert(hashA != hashB);
|
||||
assert(ledger.finalizedObservationBySlot(slot) == bytes32(0));
|
||||
|
||||
witnessC.attest(ledger, hashA);
|
||||
assert(ledger.finalizedObservationBySlot(slot) == hashA);
|
||||
|
||||
(bool ok, ) = address(witnessB).call(
|
||||
abi.encodeWithSelector(
|
||||
WitnessActor.attest.selector,
|
||||
ledger,
|
||||
hashB
|
||||
)
|
||||
);
|
||||
assert(!ok);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue