Advance forge rollout, Ethereum rails, and NBC sources

This commit is contained in:
every.channel 2026-04-01 15:58:49 -07:00
parent be26313225
commit 7d84510eac
No known key found for this signature in database
88 changed files with 11230 additions and 302 deletions

View 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
);
}
}
}

View 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
);
}
}

View 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);
}
}

View 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);
}
}