736 lines
23 KiB
Rust
736 lines
23 KiB
Rust
//! Ethereum-compatible representations and commitments for every.channel core types.
|
|
|
|
use alloy_primitives::{keccak256, B256};
|
|
use alloy_sol_types::{eip712_domain, sol, Eip712Domain, SolStruct, SolValue};
|
|
use ec_core::{
|
|
BroadcastId, ChainCommitment, ChunkId, Manifest, ManifestBody, ManifestSignature,
|
|
ManifestVariant, SourceId, StreamControlAnnouncement, StreamDescriptor, StreamKey,
|
|
StreamMetadata, StreamTransportDescriptor,
|
|
};
|
|
use std::fmt;
|
|
|
|
pub const ETHEREUM_CHAIN: &str = "ethereum";
|
|
pub const SCHEME_STREAM_ID_KECCAK: &str = "stream-id-keccak256-v1";
|
|
pub const SCHEME_STREAM_DESCRIPTOR_ABI: &str = "stream-descriptor-abi-keccak256-v1";
|
|
pub const SCHEME_CONTROL_ANNOUNCEMENT_ABI: &str = "control-announcement-abi-keccak256-v1";
|
|
pub const SCHEME_MANIFEST_DATA_ROOT: &str = "manifest-data-merkle-keccak256-v1";
|
|
pub const SCHEME_MANIFEST_BODY_ABI: &str = "manifest-body-abi-keccak256-v1";
|
|
pub const SCHEME_MANIFEST_ENVELOPE_ABI: &str = "manifest-envelope-abi-keccak256-v1";
|
|
pub const ETH_MANIFEST_SIG_ALG: &str = "secp256k1-eip712-manifest-body-v1";
|
|
pub const ZERO_B256_HEX: &str =
|
|
"0x0000000000000000000000000000000000000000000000000000000000000000";
|
|
|
|
sol! {
|
|
struct EthStreamMetadata {
|
|
string key;
|
|
string value;
|
|
}
|
|
|
|
struct EthBroadcastId {
|
|
string standard;
|
|
bool hasTransportStreamId;
|
|
uint16 transportStreamId;
|
|
bool hasProgramNumber;
|
|
uint16 programNumber;
|
|
string virtualChannel;
|
|
string callsign;
|
|
string region;
|
|
string frequency;
|
|
}
|
|
|
|
struct EthSourceId {
|
|
string kind;
|
|
string deviceId;
|
|
string channel;
|
|
}
|
|
|
|
struct EthStreamKey {
|
|
uint16 version;
|
|
bool hasBroadcast;
|
|
EthBroadcastId broadcast;
|
|
bool hasSource;
|
|
EthSourceId source;
|
|
string profile;
|
|
string variant;
|
|
}
|
|
|
|
struct EthStreamDescriptor {
|
|
string id;
|
|
string title;
|
|
string number;
|
|
string source;
|
|
EthStreamMetadata[] metadata;
|
|
}
|
|
|
|
struct EthStreamTransportDescriptor {
|
|
uint8 kind;
|
|
string url;
|
|
string endpoint;
|
|
string broadcastName;
|
|
string trackName;
|
|
}
|
|
|
|
struct EthStreamControlAnnouncement {
|
|
EthStreamDescriptor stream;
|
|
EthStreamTransportDescriptor[] transports;
|
|
uint64 updatedUnixMs;
|
|
uint64 ttlMs;
|
|
}
|
|
|
|
struct EthChunkId {
|
|
string streamId;
|
|
string epochId;
|
|
uint64 chunkIndex;
|
|
bytes32 chunkHash;
|
|
}
|
|
|
|
struct EthManifestVariant {
|
|
string variantId;
|
|
string streamId;
|
|
uint64 chunkStartIndex;
|
|
uint64 totalChunks;
|
|
bytes32 merkleRoot;
|
|
bytes32[] chunkHashes;
|
|
EthStreamMetadata[] metadata;
|
|
}
|
|
|
|
struct EthManifestBody {
|
|
string streamId;
|
|
string epochId;
|
|
uint64 chunkDurationMs;
|
|
uint64 totalChunks;
|
|
uint64 chunkStartIndex;
|
|
string encoderProfileId;
|
|
bytes32 merkleRoot;
|
|
uint64 createdUnixMs;
|
|
EthStreamMetadata[] metadata;
|
|
bytes32[] chunkHashes;
|
|
EthManifestVariant[] variants;
|
|
}
|
|
|
|
struct EthManifestSignature {
|
|
string signerId;
|
|
string alg;
|
|
bytes signature;
|
|
}
|
|
|
|
struct EthManifest {
|
|
EthManifestBody body;
|
|
bytes32 manifestId;
|
|
EthManifestSignature[] signatures;
|
|
}
|
|
|
|
struct EthObservationHeader {
|
|
bytes32 streamHash;
|
|
bytes32 epochHash;
|
|
bytes32 parentObservationHash;
|
|
bytes32 dataRoot;
|
|
bytes32 locatorHash;
|
|
uint64 observedUnixMs;
|
|
uint64 sequence;
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum EthCommitmentError {
|
|
Empty,
|
|
InvalidHex(String),
|
|
}
|
|
|
|
impl fmt::Display for EthCommitmentError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
EthCommitmentError::Empty => write!(f, "no hashes supplied"),
|
|
EthCommitmentError::InvalidHex(value) => {
|
|
write!(f, "invalid 32-byte hex value: {value}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for EthCommitmentError {}
|
|
|
|
fn commitment(scheme: &str, digest: B256) -> ChainCommitment {
|
|
ChainCommitment {
|
|
chain: ETHEREUM_CHAIN.to_string(),
|
|
scheme: scheme.to_string(),
|
|
digest: b256_hex(digest),
|
|
}
|
|
}
|
|
|
|
fn abi_commitment<T: SolValue>(scheme: &str, value: &T) -> ChainCommitment {
|
|
commitment(scheme, keccak256(value.abi_encode()))
|
|
}
|
|
|
|
pub fn b256_hex(value: B256) -> String {
|
|
format!("0x{}", hex::encode(value))
|
|
}
|
|
|
|
pub fn keccak256_bytes_hex(value: &[u8]) -> String {
|
|
b256_hex(keccak256(value))
|
|
}
|
|
|
|
pub fn parse_b256(value: &str) -> Result<B256, EthCommitmentError> {
|
|
let trimmed = value.trim().strip_prefix("0x").unwrap_or(value.trim());
|
|
let bytes =
|
|
hex::decode(trimmed).map_err(|_| EthCommitmentError::InvalidHex(value.to_string()))?;
|
|
if bytes.len() != 32 {
|
|
return Err(EthCommitmentError::InvalidHex(value.to_string()));
|
|
}
|
|
let mut out = [0u8; 32];
|
|
out.copy_from_slice(&bytes);
|
|
Ok(B256::from(out))
|
|
}
|
|
|
|
fn parse_bytes(value: &str) -> Vec<u8> {
|
|
let trimmed = value.trim().strip_prefix("0x").unwrap_or(value.trim());
|
|
hex::decode(trimmed).unwrap_or_default()
|
|
}
|
|
|
|
pub fn manifest_eip712_domain() -> Eip712Domain {
|
|
eip712_domain! {
|
|
name: "every.channel",
|
|
version: "1",
|
|
}
|
|
}
|
|
|
|
fn eth_metadata(items: &[StreamMetadata]) -> Vec<EthStreamMetadata> {
|
|
items
|
|
.iter()
|
|
.map(|item| EthStreamMetadata {
|
|
key: item.key.clone(),
|
|
value: item.value.clone(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn eth_broadcast_id(value: &BroadcastId) -> EthBroadcastId {
|
|
EthBroadcastId {
|
|
standard: value.standard.clone(),
|
|
hasTransportStreamId: value.transport_stream_id.is_some(),
|
|
transportStreamId: value.transport_stream_id.unwrap_or_default(),
|
|
hasProgramNumber: value.program_number.is_some(),
|
|
programNumber: value.program_number.unwrap_or_default(),
|
|
virtualChannel: value.virtual_channel.clone().unwrap_or_default(),
|
|
callsign: value.callsign.clone().unwrap_or_default(),
|
|
region: value.region.clone().unwrap_or_default(),
|
|
frequency: value.frequency.clone().unwrap_or_default(),
|
|
}
|
|
}
|
|
|
|
pub fn eth_source_id(value: &SourceId) -> EthSourceId {
|
|
EthSourceId {
|
|
kind: value.kind.clone(),
|
|
deviceId: value.device_id.clone().unwrap_or_default(),
|
|
channel: value.channel.clone().unwrap_or_default(),
|
|
}
|
|
}
|
|
|
|
pub fn eth_stream_key(value: &StreamKey) -> EthStreamKey {
|
|
EthStreamKey {
|
|
version: value.version,
|
|
hasBroadcast: value.broadcast.is_some(),
|
|
broadcast: value
|
|
.broadcast
|
|
.as_ref()
|
|
.map(eth_broadcast_id)
|
|
.unwrap_or_else(|| EthBroadcastId {
|
|
standard: String::new(),
|
|
hasTransportStreamId: false,
|
|
transportStreamId: 0,
|
|
hasProgramNumber: false,
|
|
programNumber: 0,
|
|
virtualChannel: String::new(),
|
|
callsign: String::new(),
|
|
region: String::new(),
|
|
frequency: String::new(),
|
|
}),
|
|
hasSource: value.source.is_some(),
|
|
source: value
|
|
.source
|
|
.as_ref()
|
|
.map(eth_source_id)
|
|
.unwrap_or_else(|| EthSourceId {
|
|
kind: String::new(),
|
|
deviceId: String::new(),
|
|
channel: String::new(),
|
|
}),
|
|
profile: value.profile.clone().unwrap_or_default(),
|
|
variant: value.variant.clone().unwrap_or_default(),
|
|
}
|
|
}
|
|
|
|
pub fn eth_stream_descriptor(value: &StreamDescriptor) -> EthStreamDescriptor {
|
|
EthStreamDescriptor {
|
|
id: value.id.0.clone(),
|
|
title: value.title.clone(),
|
|
number: value.number.clone().unwrap_or_default(),
|
|
source: value.source.clone(),
|
|
metadata: eth_metadata(&value.metadata),
|
|
}
|
|
}
|
|
|
|
pub fn eth_stream_transport_descriptor(
|
|
value: &StreamTransportDescriptor,
|
|
) -> EthStreamTransportDescriptor {
|
|
match value {
|
|
StreamTransportDescriptor::RelayMoq {
|
|
url,
|
|
broadcast_name,
|
|
track_name,
|
|
} => EthStreamTransportDescriptor {
|
|
kind: 0,
|
|
url: url.clone(),
|
|
endpoint: String::new(),
|
|
broadcastName: broadcast_name.clone(),
|
|
trackName: track_name.clone(),
|
|
},
|
|
StreamTransportDescriptor::IrohDirect {
|
|
endpoint,
|
|
broadcast_name,
|
|
track_name,
|
|
} => EthStreamTransportDescriptor {
|
|
kind: 1,
|
|
url: String::new(),
|
|
endpoint: endpoint.clone(),
|
|
broadcastName: broadcast_name.clone(),
|
|
trackName: track_name.clone(),
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn eth_stream_control_announcement(
|
|
value: &StreamControlAnnouncement,
|
|
) -> EthStreamControlAnnouncement {
|
|
EthStreamControlAnnouncement {
|
|
stream: eth_stream_descriptor(&value.stream),
|
|
transports: value
|
|
.transports
|
|
.iter()
|
|
.map(eth_stream_transport_descriptor)
|
|
.collect(),
|
|
updatedUnixMs: value.updated_unix_ms,
|
|
ttlMs: value.ttl_ms,
|
|
}
|
|
}
|
|
|
|
pub fn eth_chunk_id(value: &ChunkId) -> Result<EthChunkId, EthCommitmentError> {
|
|
Ok(EthChunkId {
|
|
streamId: value.stream_id.0.clone(),
|
|
epochId: value.epoch_id.clone(),
|
|
chunkIndex: value.chunk_index,
|
|
chunkHash: parse_b256(&value.chunk_hash)?,
|
|
})
|
|
}
|
|
|
|
pub fn eth_manifest_variant(
|
|
value: &ManifestVariant,
|
|
) -> Result<EthManifestVariant, EthCommitmentError> {
|
|
Ok(EthManifestVariant {
|
|
variantId: value.variant_id.clone(),
|
|
streamId: value.stream_id.0.clone(),
|
|
chunkStartIndex: value.chunk_start_index,
|
|
totalChunks: value.total_chunks,
|
|
merkleRoot: parse_b256(&value.merkle_root)?,
|
|
chunkHashes: value
|
|
.chunk_hashes
|
|
.iter()
|
|
.map(|hash| parse_b256(hash))
|
|
.collect::<Result<Vec<_>, _>>()?,
|
|
metadata: eth_metadata(&value.metadata),
|
|
})
|
|
}
|
|
|
|
pub fn eth_manifest_body(value: &ManifestBody) -> Result<EthManifestBody, EthCommitmentError> {
|
|
let variants = value
|
|
.variants
|
|
.as_ref()
|
|
.map(|variants| {
|
|
variants
|
|
.iter()
|
|
.map(eth_manifest_variant)
|
|
.collect::<Result<Vec<_>, _>>()
|
|
})
|
|
.transpose()?
|
|
.unwrap_or_default();
|
|
|
|
Ok(EthManifestBody {
|
|
streamId: value.stream_id.0.clone(),
|
|
epochId: value.epoch_id.clone(),
|
|
chunkDurationMs: value.chunk_duration_ms,
|
|
totalChunks: value.total_chunks,
|
|
chunkStartIndex: value.chunk_start_index,
|
|
encoderProfileId: value.encoder_profile_id.clone(),
|
|
merkleRoot: parse_b256(&value.merkle_root)?,
|
|
createdUnixMs: value.created_unix_ms,
|
|
metadata: eth_metadata(&value.metadata),
|
|
chunkHashes: value
|
|
.chunk_hashes
|
|
.iter()
|
|
.map(|hash| parse_b256(hash))
|
|
.collect::<Result<Vec<_>, _>>()?,
|
|
variants,
|
|
})
|
|
}
|
|
|
|
pub fn manifest_body_eip712_signing_hash(value: &ManifestBody) -> Result<B256, EthCommitmentError> {
|
|
Ok(eth_manifest_body(value)?.eip712_signing_hash(&manifest_eip712_domain()))
|
|
}
|
|
|
|
pub fn eth_manifest_signature(value: &ManifestSignature) -> EthManifestSignature {
|
|
EthManifestSignature {
|
|
signerId: value.signer_id.clone(),
|
|
alg: value.alg.clone(),
|
|
signature: parse_bytes(&value.signature).into(),
|
|
}
|
|
}
|
|
|
|
pub fn eth_manifest(value: &Manifest) -> Result<EthManifest, EthCommitmentError> {
|
|
Ok(EthManifest {
|
|
body: eth_manifest_body(&value.body)?,
|
|
manifestId: parse_b256(&value.manifest_id)?,
|
|
signatures: value
|
|
.signatures
|
|
.iter()
|
|
.map(eth_manifest_signature)
|
|
.collect(),
|
|
})
|
|
}
|
|
|
|
fn keccak_merkle_root(leaves: &[B256]) -> Result<B256, EthCommitmentError> {
|
|
if leaves.is_empty() {
|
|
return Err(EthCommitmentError::Empty);
|
|
}
|
|
let mut nodes = leaves.to_vec();
|
|
while nodes.len() > 1 {
|
|
if nodes.len() % 2 == 1 {
|
|
if let Some(last) = nodes.last().copied() {
|
|
nodes.push(last);
|
|
}
|
|
}
|
|
let mut parents = Vec::with_capacity(nodes.len() / 2);
|
|
for pair in nodes.chunks(2) {
|
|
let mut merged = [0u8; 64];
|
|
merged[..32].copy_from_slice(pair[0].as_slice());
|
|
merged[32..].copy_from_slice(pair[1].as_slice());
|
|
parents.push(keccak256(merged));
|
|
}
|
|
nodes = parents;
|
|
}
|
|
Ok(nodes[0])
|
|
}
|
|
|
|
pub fn ethereum_merkle_root_from_hashes(hashes: &[String]) -> Result<String, EthCommitmentError> {
|
|
let leaves = hashes
|
|
.iter()
|
|
.map(|hash| parse_b256(hash))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
Ok(format!("0x{}", hex::encode(keccak_merkle_root(&leaves)?)))
|
|
}
|
|
|
|
pub fn ethereum_merkle_proof_for_index(
|
|
hashes: &[String],
|
|
index: usize,
|
|
) -> Result<Vec<String>, EthCommitmentError> {
|
|
if hashes.is_empty() {
|
|
return Err(EthCommitmentError::Empty);
|
|
}
|
|
if index >= hashes.len() {
|
|
return Err(EthCommitmentError::InvalidHex(format!(
|
|
"index {index} out of bounds"
|
|
)));
|
|
}
|
|
|
|
let mut nodes = hashes
|
|
.iter()
|
|
.map(|hash| parse_b256(hash))
|
|
.collect::<Result<Vec<_>, _>>()?;
|
|
let mut proof = Vec::new();
|
|
let mut pos = index;
|
|
while nodes.len() > 1 {
|
|
if nodes.len() % 2 == 1 {
|
|
if let Some(last) = nodes.last().copied() {
|
|
nodes.push(last);
|
|
}
|
|
}
|
|
let sibling_index = if pos % 2 == 0 { pos + 1 } else { pos - 1 };
|
|
let sibling = nodes[sibling_index];
|
|
proof.push(format!("0x{}", hex::encode(sibling)));
|
|
|
|
let mut parents = Vec::with_capacity(nodes.len() / 2);
|
|
for pair in nodes.chunks(2) {
|
|
let mut merged = [0u8; 64];
|
|
merged[..32].copy_from_slice(pair[0].as_slice());
|
|
merged[32..].copy_from_slice(pair[1].as_slice());
|
|
parents.push(keccak256(merged));
|
|
}
|
|
nodes = parents;
|
|
pos /= 2;
|
|
}
|
|
|
|
Ok(proof)
|
|
}
|
|
|
|
pub fn verify_ethereum_merkle_proof(
|
|
leaf_hash: &str,
|
|
mut index: usize,
|
|
branch: &[String],
|
|
expected_root: &str,
|
|
) -> bool {
|
|
let Ok(mut acc) = parse_b256(leaf_hash) else {
|
|
return false;
|
|
};
|
|
for sibling_hex in branch {
|
|
let Ok(sibling) = parse_b256(sibling_hex) else {
|
|
return false;
|
|
};
|
|
let (left, right) = if index % 2 == 0 {
|
|
(acc, sibling)
|
|
} else {
|
|
(sibling, acc)
|
|
};
|
|
let mut merged = [0u8; 64];
|
|
merged[..32].copy_from_slice(left.as_slice());
|
|
merged[32..].copy_from_slice(right.as_slice());
|
|
acc = keccak256(merged);
|
|
index /= 2;
|
|
}
|
|
match parse_b256(expected_root) {
|
|
Ok(root) => acc == root,
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
pub fn stream_id_commitment(stream_id: &str) -> ChainCommitment {
|
|
commitment(SCHEME_STREAM_ID_KECCAK, keccak256(stream_id.as_bytes()))
|
|
}
|
|
|
|
pub fn broadcast_id_commitment(value: &BroadcastId) -> ChainCommitment {
|
|
abi_commitment("broadcast-id-abi-keccak256-v1", ð_broadcast_id(value))
|
|
}
|
|
|
|
pub fn stream_key_commitment(value: &StreamKey) -> ChainCommitment {
|
|
abi_commitment("stream-key-abi-keccak256-v1", ð_stream_key(value))
|
|
}
|
|
|
|
pub fn stream_descriptor_commitments(value: &StreamDescriptor) -> Vec<ChainCommitment> {
|
|
vec![
|
|
stream_id_commitment(&value.id.0),
|
|
abi_commitment(SCHEME_STREAM_DESCRIPTOR_ABI, ð_stream_descriptor(value)),
|
|
]
|
|
}
|
|
|
|
pub fn control_announcement_commitments(value: &StreamControlAnnouncement) -> Vec<ChainCommitment> {
|
|
vec![abi_commitment(
|
|
SCHEME_CONTROL_ANNOUNCEMENT_ABI,
|
|
ð_stream_control_announcement(value),
|
|
)]
|
|
}
|
|
|
|
fn manifest_data_root_commitment(
|
|
body: &ManifestBody,
|
|
) -> Result<ChainCommitment, EthCommitmentError> {
|
|
let root = if let Some(variants) = body
|
|
.variants
|
|
.as_ref()
|
|
.filter(|variants| !variants.is_empty())
|
|
{
|
|
let roots = variants
|
|
.iter()
|
|
.map(|variant| variant.merkle_root.clone())
|
|
.collect::<Vec<_>>();
|
|
ethereum_merkle_root_from_hashes(&roots)?
|
|
} else {
|
|
ethereum_merkle_root_from_hashes(&body.chunk_hashes)?
|
|
};
|
|
Ok(ChainCommitment {
|
|
chain: ETHEREUM_CHAIN.to_string(),
|
|
scheme: SCHEME_MANIFEST_DATA_ROOT.to_string(),
|
|
digest: root,
|
|
})
|
|
}
|
|
|
|
pub fn manifest_commitments(value: &Manifest) -> Result<Vec<ChainCommitment>, EthCommitmentError> {
|
|
let eth_body = eth_manifest_body(&value.body)?;
|
|
let eth_manifest = eth_manifest(value)?;
|
|
Ok(vec![
|
|
manifest_data_root_commitment(&value.body)?,
|
|
abi_commitment(SCHEME_MANIFEST_BODY_ABI, ð_body),
|
|
abi_commitment(SCHEME_MANIFEST_ENVELOPE_ABI, ð_manifest),
|
|
])
|
|
}
|
|
|
|
pub fn manifest_commitment_digest(
|
|
value: &Manifest,
|
|
scheme: &str,
|
|
) -> Result<Option<String>, EthCommitmentError> {
|
|
Ok(manifest_commitments(value)?
|
|
.into_iter()
|
|
.find(|commitment| commitment.scheme == scheme)
|
|
.map(|commitment| commitment.digest))
|
|
}
|
|
|
|
pub fn manifest_observation_header(
|
|
value: &Manifest,
|
|
parent_observation_hash: Option<&str>,
|
|
locator_hash: &str,
|
|
sequence: u64,
|
|
) -> Result<EthObservationHeader, EthCommitmentError> {
|
|
let data_root = manifest_commitment_digest(value, SCHEME_MANIFEST_DATA_ROOT)?
|
|
.ok_or(EthCommitmentError::Empty)?;
|
|
|
|
Ok(EthObservationHeader {
|
|
streamHash: keccak256(value.body.stream_id.0.as_bytes()),
|
|
epochHash: keccak256(value.body.epoch_id.as_bytes()),
|
|
parentObservationHash: parse_b256(parent_observation_hash.unwrap_or(ZERO_B256_HEX))?,
|
|
dataRoot: parse_b256(&data_root)?,
|
|
locatorHash: parse_b256(locator_hash)?,
|
|
observedUnixMs: value.body.created_unix_ms,
|
|
sequence,
|
|
})
|
|
}
|
|
|
|
pub fn observation_header_hash(value: &EthObservationHeader) -> String {
|
|
b256_hex(keccak256(value.abi_encode()))
|
|
}
|
|
|
|
pub fn observation_slot_hash(
|
|
stream_hash: &str,
|
|
epoch_hash: &str,
|
|
) -> Result<String, EthCommitmentError> {
|
|
let stream_hash = parse_b256(stream_hash)?;
|
|
let epoch_hash = parse_b256(epoch_hash)?;
|
|
Ok(b256_hex(keccak256((stream_hash, epoch_hash).abi_encode())))
|
|
}
|
|
|
|
pub fn manifest_commitments_match(value: &Manifest) -> Result<bool, EthCommitmentError> {
|
|
let present = value
|
|
.commitments
|
|
.iter()
|
|
.filter(|item| item.chain == ETHEREUM_CHAIN)
|
|
.collect::<Vec<_>>();
|
|
if present.is_empty() {
|
|
return Ok(true);
|
|
}
|
|
let expected = manifest_commitments(value)?;
|
|
Ok(present.into_iter().all(|actual| expected.contains(actual)))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ec_core::{ManifestBody, StreamId};
|
|
|
|
fn sample_body() -> ManifestBody {
|
|
let chunk_hashes = vec![
|
|
blake3::hash(b"chunk0").to_hex().to_string(),
|
|
blake3::hash(b"chunk1").to_hex().to_string(),
|
|
];
|
|
let merkle_root = ec_core::merkle_root_from_hashes(&chunk_hashes).unwrap();
|
|
ManifestBody {
|
|
stream_id: StreamId("ec/stream/v1/broadcast/atsc/tsid-42/program-3".to_string()),
|
|
epoch_id: "epoch-1".to_string(),
|
|
chunk_duration_ms: 2000,
|
|
total_chunks: 2,
|
|
chunk_start_index: 10,
|
|
encoder_profile_id: "deterministic-h264-aac".to_string(),
|
|
merkle_root,
|
|
created_unix_ms: 1234,
|
|
metadata: vec![StreamMetadata {
|
|
key: "callsign".to_string(),
|
|
value: "KCBS".to_string(),
|
|
}],
|
|
chunk_hashes,
|
|
variants: None,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn keccak_merkle_root_and_proof_roundtrip() {
|
|
let body = sample_body();
|
|
let root = ethereum_merkle_root_from_hashes(&body.chunk_hashes).unwrap();
|
|
let proof = ethereum_merkle_proof_for_index(&body.chunk_hashes, 1).unwrap();
|
|
assert!(verify_ethereum_merkle_proof(
|
|
&body.chunk_hashes[1],
|
|
1,
|
|
&proof,
|
|
&root
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_commitments_are_stable_and_match_present_values() {
|
|
let body = sample_body();
|
|
let manifest_id = body.manifest_id().unwrap();
|
|
let mut manifest = Manifest {
|
|
body,
|
|
manifest_id,
|
|
signatures: Vec::new(),
|
|
commitments: Vec::new(),
|
|
};
|
|
let commitments = manifest_commitments(&manifest).unwrap();
|
|
assert_eq!(commitments.len(), 3);
|
|
manifest.commitments = commitments.clone();
|
|
assert!(manifest_commitments_match(&manifest).unwrap());
|
|
manifest.commitments[0].digest = "0xdeadbeef".to_string();
|
|
assert!(!manifest_commitments_match(&manifest).unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_body_eip712_hash_is_stable() {
|
|
let body = sample_body();
|
|
let h1 = manifest_body_eip712_signing_hash(&body).unwrap();
|
|
let h2 = manifest_body_eip712_signing_hash(&body).unwrap();
|
|
assert_eq!(h1, h2);
|
|
}
|
|
|
|
#[test]
|
|
fn manifest_observation_header_uses_manifest_data_root() {
|
|
let body = sample_body();
|
|
let manifest_id = body.manifest_id().unwrap();
|
|
let mut manifest = Manifest {
|
|
body,
|
|
manifest_id,
|
|
signatures: Vec::new(),
|
|
commitments: Vec::new(),
|
|
};
|
|
manifest.commitments = manifest_commitments(&manifest).unwrap();
|
|
|
|
let locator_hash = keccak256_bytes_hex(b"locator");
|
|
let header = manifest_observation_header(&manifest, None, &locator_hash, 7).unwrap();
|
|
assert_eq!(header.sequence, 7);
|
|
assert_eq!(header.observedUnixMs, manifest.body.created_unix_ms);
|
|
assert_eq!(
|
|
b256_hex(header.dataRoot),
|
|
manifest_commitment_digest(&manifest, SCHEME_MANIFEST_DATA_ROOT)
|
|
.unwrap()
|
|
.unwrap()
|
|
);
|
|
assert_eq!(
|
|
observation_slot_hash(&b256_hex(header.streamHash), &b256_hex(header.epochHash))
|
|
.unwrap(),
|
|
b256_hex(keccak256(
|
|
(header.streamHash, header.epochHash).abi_encode()
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stream_descriptor_commitments_include_stream_id_and_descriptor_hashes() {
|
|
let descriptor = StreamDescriptor {
|
|
id: StreamId("ec/stream/v1/source/test/device-a/channel-b".to_string()),
|
|
title: "Test".to_string(),
|
|
number: Some("2.1".to_string()),
|
|
source: "control".to_string(),
|
|
metadata: vec![StreamMetadata {
|
|
key: "broadcast".to_string(),
|
|
value: "la-nbc".to_string(),
|
|
}],
|
|
commitments: Vec::new(),
|
|
};
|
|
let commitments = stream_descriptor_commitments(&descriptor);
|
|
assert_eq!(commitments.len(), 2);
|
|
assert_eq!(commitments[0].scheme, SCHEME_STREAM_ID_KECCAK);
|
|
assert_eq!(commitments[1].scheme, SCHEME_STREAM_DESCRIPTOR_ABI);
|
|
}
|
|
}
|