use std::collections::BTreeMap;
use std::sync::Arc;
use sha2::{Digest, Sha256};
use crate::error::VerifyError;
use crate::plan::ChunkLayout;
#[derive(Debug, Default)]
pub struct ResourceHasher {
hasher: Sha256,
next_offset: u64,
pending: BTreeMap<u64, Vec<u8>>,
}
impl ResourceHasher {
pub fn new() -> Self {
ResourceHasher::default()
}
pub fn feed(&mut self, offset: u64, bytes: Vec<u8>) {
if offset < self.next_offset {
return; }
self.pending.insert(offset, bytes);
while let Some(chunk) = self.pending.remove(&self.next_offset) {
self.hasher.update(&chunk);
self.next_offset = self.next_offset.saturating_add(chunk.len() as u64);
}
}
pub fn hashed_len(&self) -> u64 {
self.next_offset
}
pub fn has_gap(&self) -> bool {
!self.pending.is_empty()
}
pub fn finalize(self) -> [u8; 32] {
self.hasher.finalize().into()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResourceCommitment {
pub layout: ChunkLayout,
pub total_length: u64,
pub root: Option<String>,
pub inclusion_proof: Option<String>,
}
impl ResourceCommitment {
pub fn from_first_frame(
total_length: u64,
chunk_lens: Vec<u64>,
root: Option<String>,
inclusion_proof: Option<String>,
) -> Result<Self, VerifyError> {
let layout = ChunkLayout::new(chunk_lens);
if layout.total_length() != total_length {
return Err(VerifyError::Metadata(format!(
"chunk_lens sum {} != total_length {}",
layout.total_length(),
total_length
)));
}
Ok(ResourceCommitment {
layout,
total_length,
root,
inclusion_proof,
})
}
pub fn check_consistent(
&self,
total_length: Option<u64>,
chunk_lens: Option<&[u64]>,
root: Option<&str>,
) -> Result<(), VerifyError> {
if let Some(tl) = total_length {
if tl != self.total_length {
return Err(VerifyError::Metadata(format!(
"total_length {tl} != committed {}",
self.total_length
)));
}
}
if let Some(cl) = chunk_lens {
if cl != self.layout.chunk_lens() {
return Err(VerifyError::Metadata("chunk_lens differ".into()));
}
}
if let (Some(r), Some(committed)) = (root, self.root.as_deref()) {
if r != committed {
return Err(VerifyError::Metadata(format!(
"root {r} != committed {committed}"
)));
}
}
Ok(())
}
}
pub trait ProofVerifier: Send + Sync {
fn verify_inclusion(
&self,
resource_leaf: &[u8; 32],
inclusion_proof: Option<&str>,
root: Option<&str>,
) -> bool;
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, Default)]
pub struct StructuralOnlyProofVerifier;
impl ProofVerifier for StructuralOnlyProofVerifier {
fn verify_inclusion(
&self,
_resource_leaf: &[u8; 32],
_inclusion_proof: Option<&str>,
_root: Option<&str>,
) -> bool {
true
}
}
pub trait Verifier: Send + Sync {
fn verify_range(
&self,
commitment: &ResourceCommitment,
first_chunk_index: u64,
expected_len: u64,
bytes: &[u8],
) -> Result<(), VerifyError>;
fn verify_resource(
&self,
commitment: &ResourceCommitment,
full: &[u8],
) -> Result<(), VerifyError>;
fn verify_resource_leaf(
&self,
commitment: &ResourceCommitment,
leaf: &[u8; 32],
assembled_len: u64,
) -> Result<(), VerifyError>;
}
pub struct MerkleVerifier {
proof: Arc<dyn ProofVerifier>,
}
impl MerkleVerifier {
pub fn with_proof_verifier(proof: Arc<dyn ProofVerifier>) -> Self {
MerkleVerifier { proof }
}
#[doc(hidden)]
pub fn insecure_structural_only() -> Self {
MerkleVerifier {
proof: Arc::new(StructuralOnlyProofVerifier),
}
}
pub fn resource_leaf(full: &[u8]) -> [u8; 32] {
let digest = Sha256::digest(full);
digest.into()
}
}
impl Verifier for MerkleVerifier {
fn verify_range(
&self,
commitment: &ResourceCommitment,
first_chunk_index: u64,
expected_len: u64,
bytes: &[u8],
) -> Result<(), VerifyError> {
if bytes.len() as u64 != expected_len {
return Err(VerifyError::Length {
expected: expected_len,
actual: bytes.len() as u64,
});
}
let start = first_chunk_index as usize;
let layout = &commitment.layout;
if start >= layout.chunk_count() {
return Err(VerifyError::Alignment(format!(
"chunk_index {start} out of range (chunk_count {})",
layout.chunk_count()
)));
}
let offset = layout
.chunk_offset(start)
.ok_or_else(|| VerifyError::Alignment("chunk_index has no offset".into()))?;
let (cs, ce) = layout.chunks_for_range(offset, bytes.len() as u64)?;
debug_assert_eq!(cs, start);
let _ = ce;
Ok(())
}
fn verify_resource(
&self,
commitment: &ResourceCommitment,
full: &[u8],
) -> Result<(), VerifyError> {
let leaf = MerkleVerifier::resource_leaf(full);
self.verify_resource_leaf(commitment, &leaf, full.len() as u64)
}
fn verify_resource_leaf(
&self,
commitment: &ResourceCommitment,
leaf: &[u8; 32],
assembled_len: u64,
) -> Result<(), VerifyError> {
if assembled_len != commitment.total_length {
return Err(VerifyError::Length {
expected: commitment.total_length,
actual: assembled_len,
});
}
if !self.proof.verify_inclusion(
leaf,
commitment.inclusion_proof.as_deref(),
commitment.root.as_deref(),
) {
return Err(VerifyError::Root);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn commitment(chunk_lens: Vec<u64>) -> ResourceCommitment {
let total = chunk_lens.iter().sum();
ResourceCommitment::from_first_frame(total, chunk_lens, Some("aa".repeat(32)), None)
.unwrap()
}
#[test]
fn from_first_frame_rejects_inconsistent_total() {
let err = ResourceCommitment::from_first_frame(999, vec![10, 20], None, None);
assert!(matches!(err, Err(VerifyError::Metadata(_))));
}
#[test]
fn resource_hasher_matches_concat_hash_regardless_of_feed_order() {
let full: Vec<u8> = (0..90u16).map(|i| i as u8).collect();
let expect = MerkleVerifier::resource_leaf(&full);
let mut h = ResourceHasher::new();
h.feed(60, full[60..90].to_vec());
assert!(
h.has_gap(),
"range at 60 is ahead of the frontier → buffered"
);
assert_eq!(h.hashed_len(), 0);
h.feed(0, full[0..30].to_vec());
assert_eq!(h.hashed_len(), 30);
assert!(h.has_gap(), "range at 60 still buffered, 30..60 missing");
h.feed(30, full[30..60].to_vec());
assert!(!h.has_gap(), "the gap filled → everything drained");
assert_eq!(h.hashed_len(), 90);
assert_eq!(h.finalize(), expect);
let mut h2 = ResourceHasher::new();
for off in [0u64, 30, 60] {
h2.feed(off, full[off as usize..off as usize + 30].to_vec());
}
assert_eq!(h2.hashed_len(), 90);
assert_eq!(h2.finalize(), expect);
}
#[test]
fn resource_hasher_ignores_a_range_before_the_frontier() {
let mut h = ResourceHasher::new();
h.feed(0, vec![1u8; 10]);
assert_eq!(h.hashed_len(), 10);
h.feed(0, vec![1u8; 10]);
assert_eq!(h.hashed_len(), 10);
h.feed(10, vec![2u8; 10]);
assert_eq!(h.hashed_len(), 20);
let mut concat = vec![1u8; 10];
concat.extend_from_slice(&[2u8; 10]);
assert_eq!(h.finalize(), MerkleVerifier::resource_leaf(&concat));
}
#[test]
fn verify_resource_leaf_length_and_root_binding() {
let c = commitment(vec![10, 20]);
let correct = vec![3u8; 30];
let leaf = MerkleVerifier::resource_leaf(&correct);
let v = MerkleVerifier::insecure_structural_only();
assert!(matches!(
v.verify_resource_leaf(&c, &leaf, 20),
Err(VerifyError::Length { .. })
));
assert!(v.verify_resource_leaf(&c, &leaf, 30).is_ok());
struct OnlyLeaf([u8; 32]);
impl ProofVerifier for OnlyLeaf {
fn verify_inclusion(&self, l: &[u8; 32], _p: Option<&str>, _r: Option<&str>) -> bool {
l == &self.0
}
}
let v2 = MerkleVerifier::with_proof_verifier(Arc::new(OnlyLeaf(leaf)));
assert!(v2.verify_resource_leaf(&c, &leaf, 30).is_ok());
assert!(matches!(
v2.verify_resource_leaf(&c, &[0u8; 32], 30),
Err(VerifyError::Root)
));
}
#[test]
fn verify_range_accepts_whole_chunks() {
let c = commitment(vec![10, 20, 5]);
let v = MerkleVerifier::insecure_structural_only();
assert!(v.verify_range(&c, 0, 10, &[0u8; 10]).is_ok());
assert!(v.verify_range(&c, 1, 25, &[0u8; 25]).is_ok());
}
#[test]
fn verify_range_rejects_wrong_length() {
let c = commitment(vec![10, 20, 5]);
let v = MerkleVerifier::insecure_structural_only();
assert!(matches!(
v.verify_range(&c, 0, 10, &[0u8; 9]),
Err(VerifyError::Length {
expected: 10,
actual: 9
})
));
}
#[test]
fn verify_range_rejects_boundary_aligned_short_range() {
let c = commitment(vec![10, 20, 5]);
let v = MerkleVerifier::insecure_structural_only();
assert!(matches!(
v.verify_range(&c, 0, 30, &[0u8; 10]),
Err(VerifyError::Length {
expected: 30,
actual: 10
})
));
assert!(matches!(
v.verify_range(&c, 0, 10, &[0u8; 30]),
Err(VerifyError::Length {
expected: 10,
actual: 30
})
));
}
#[test]
fn verify_range_rejects_out_of_range_chunk_index() {
let c = commitment(vec![10]);
let v = MerkleVerifier::insecure_structural_only();
assert!(matches!(
v.verify_range(&c, 5, 10, &[0u8; 10]),
Err(VerifyError::Alignment(_))
));
}
#[test]
fn verify_resource_length_mismatch() {
let c = commitment(vec![10, 20]);
let v = MerkleVerifier::insecure_structural_only();
assert!(matches!(
v.verify_resource(&c, &[0u8; 5]),
Err(VerifyError::Length { .. })
));
}
#[test]
fn insecure_structural_only_is_fail_open_on_the_root() {
let c = commitment(vec![10, 20]);
let v = MerkleVerifier::insecure_structural_only();
assert!(v.verify_resource(&c, &[0u8; 30]).is_ok());
assert!(v.verify_resource(&c, &[0xFFu8; 30]).is_ok());
}
#[test]
fn verify_resource_binds_to_root_with_real_proof_verifier() {
struct OnlyLeaf([u8; 32]);
impl ProofVerifier for OnlyLeaf {
fn verify_inclusion(
&self,
resource_leaf: &[u8; 32],
_p: Option<&str>,
_r: Option<&str>,
) -> bool {
resource_leaf == &self.0
}
}
let correct = vec![7u8; 30];
let leaf = MerkleVerifier::resource_leaf(&correct);
let v = MerkleVerifier::with_proof_verifier(Arc::new(OnlyLeaf(leaf)));
let c = commitment(vec![10, 20]);
assert!(v.verify_resource(&c, &correct).is_ok());
assert!(matches!(
v.verify_resource(&c, &[8u8; 30]),
Err(VerifyError::Root)
));
}
#[test]
fn commitment_consistency_check() {
let c = commitment(vec![10, 20, 5]);
assert!(c
.check_consistent(Some(35), Some(&[10, 20, 5]), Some(&"aa".repeat(32)))
.is_ok());
assert!(matches!(
c.check_consistent(Some(99), None, None),
Err(VerifyError::Metadata(_))
));
assert!(matches!(
c.check_consistent(None, Some(&[1, 2]), None),
Err(VerifyError::Metadata(_))
));
assert!(matches!(
c.check_consistent(None, None, Some(&"bb".repeat(32))),
Err(VerifyError::Metadata(_))
));
}
#[test]
fn resource_leaf_is_sha256_untagged() {
let leaf = MerkleVerifier::resource_leaf(b"hello");
let expect: [u8; 32] = Sha256::digest(b"hello").into();
assert_eq!(leaf, expect);
}
}