Wire HDHomeRun observations and recover Forge OP Stack

This commit is contained in:
every.channel 2026-05-03 20:24:04 -07:00
parent 8065860449
commit 0d86104762
No known key found for this signature in database
18 changed files with 1613 additions and 58 deletions

View file

@ -17,6 +17,8 @@ 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 {
@ -117,6 +119,16 @@ sol! {
bytes32 manifestId;
EthManifestSignature[] signatures;
}
struct EthObservationHeader {
bytes32 streamHash;
bytes32 epochHash;
bytes32 parentObservationHash;
bytes32 dataRoot;
bytes32 locatorHash;
uint64 observedUnixMs;
uint64 sequence;
}
}
#[derive(Debug, Clone)]
@ -142,7 +154,7 @@ fn commitment(scheme: &str, digest: B256) -> ChainCommitment {
ChainCommitment {
chain: ETHEREUM_CHAIN.to_string(),
scheme: scheme.to_string(),
digest: format!("0x{}", hex::encode(digest)),
digest: b256_hex(digest),
}
}
@ -150,7 +162,15 @@ fn abi_commitment<T: SolValue>(scheme: &str, value: &T) -> ChainCommitment {
commitment(scheme, keccak256(value.abi_encode()))
}
fn parse_b256(value: &str) -> Result<B256, EthCommitmentError> {
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()))?;
@ -540,6 +560,49 @@ pub fn manifest_commitments(value: &Manifest) -> Result<Vec<ChainCommitment>, Et
])
}
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
@ -621,6 +684,37 @@ mod tests {
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 {