use std::collections::HashMap;
use hemera::OUTPUT_BYTES;
use crate::hash::HashBackend;
use crate::tree::{BaoChunk, BaoTree, BlockSize, CHUNK_SIZE};
const PAIR_SIZE: usize = OUTPUT_BYTES * 2;
#[derive(Debug, Clone)]
pub struct Outboard<H: Clone> {
pub root: H,
pub data: Vec<u8>,
pub tree: BaoTree,
}
pub fn outboard<B: HashBackend>(
backend: &B,
data: &[u8],
block_size: BlockSize,
) -> Outboard<B::Hash> {
let tree = BaoTree::new(data.len() as u64, block_size);
let blocks = tree.blocks();
let bs = block_size.bytes();
if blocks <= 1 {
let root = hash_block(backend, data, 0, true, bs);
return Outboard {
root,
data: Vec::new(),
tree,
};
}
let post_order = tree.post_order_chunks();
let mut hash_stack: Vec<B::Hash> = Vec::new();
let mut node_to_children: HashMap<u64, (B::Hash, B::Hash)> = HashMap::new();
for chunk in &post_order {
match chunk {
BaoChunk::Leaf {
start_chunk,
size,
is_root,
} => {
let byte_start = *start_chunk * CHUNK_SIZE as u64;
let byte_end = (byte_start + *size as u64).min(data.len() as u64);
let chunk_data = if byte_start < data.len() as u64 {
&data[byte_start as usize..byte_end as usize]
} else {
&[]
};
let block_cv = hash_block(backend, chunk_data, *start_chunk, *is_root, bs);
hash_stack.push(block_cv);
}
BaoChunk::Parent { node, is_root, .. } => {
let right = hash_stack.pop().expect("missing right child hash");
let left = hash_stack.pop().expect("missing left child hash");
node_to_children.insert(node.0, (left.clone(), right.clone()));
let parent = backend.parent_hash(&left, &right, *is_root);
hash_stack.push(parent);
}
}
}
let root = hash_stack
.pop()
.expect("hash stack should have root after traversal");
debug_assert!(hash_stack.is_empty());
let pre_order = tree.pre_order_chunks();
let mut outboard_data = Vec::with_capacity(node_to_children.len() * backend.hash_size() * 2);
for chunk in &pre_order {
if let BaoChunk::Parent { node, .. } = chunk
&& let Some((left, right)) = node_to_children.get(&node.0)
{
outboard_data.extend_from_slice(left.as_ref());
outboard_data.extend_from_slice(right.as_ref());
}
}
Outboard {
root,
data: outboard_data,
tree,
}
}
fn hash_block<B: HashBackend>(
backend: &B,
data: &[u8],
start_chunk: u64,
is_root: bool,
block_bytes: usize,
) -> B::Hash {
if data.is_empty() {
return backend.chunk_hash(&[], start_chunk, is_root);
}
let mut chunk_hashes: Vec<B::Hash> = Vec::new();
let mut offset = 0usize;
let mut counter = start_chunk;
while offset < data.len() {
let end = (offset + CHUNK_SIZE).min(data.len());
let chunk_data = &data[offset..end];
let is_single_chunk = data.len() <= CHUNK_SIZE && is_root;
chunk_hashes.push(backend.chunk_hash(chunk_data, counter, is_single_chunk));
offset += CHUNK_SIZE;
counter += 1;
}
if chunk_hashes.len() == 1 {
return chunk_hashes.into_iter().next().unwrap();
}
let _ = block_bytes;
let mut level = chunk_hashes;
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
let mut i = 0;
while i < level.len() {
if i + 1 < level.len() {
let parent = backend.parent_hash(&level[i], &level[i + 1], false);
next.push(parent);
} else {
next.push(level[i].clone());
}
i += 2;
}
level = next;
}
level.into_iter().next().unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash::Poseidon2Backend;
#[test]
fn outboard_single_block() {
let backend = Poseidon2Backend;
let data = vec![0x42u8; CHUNK_SIZE];
let ob = outboard(&backend, &data, BlockSize::ZERO);
assert!(ob.data.is_empty());
assert_eq!(ob.root, backend.chunk_hash(&data, 0, true));
}
#[test]
fn outboard_two_blocks() {
let backend = Poseidon2Backend;
let data = vec![0x42u8; CHUNK_SIZE * 2];
let ob = outboard(&backend, &data, BlockSize::ZERO);
assert_eq!(ob.data.len(), PAIR_SIZE);
let left = backend.chunk_hash(&data[..CHUNK_SIZE], 0, false);
let right = backend.chunk_hash(&data[CHUNK_SIZE..], 1, false);
let expected_root = backend.parent_hash(&left, &right, true);
assert_eq!(ob.root, expected_root);
assert_eq!(&ob.data[..OUTPUT_BYTES], left.as_ref());
assert_eq!(&ob.data[OUTPUT_BYTES..PAIR_SIZE], right.as_ref());
}
#[test]
fn outboard_four_blocks() {
let backend = Poseidon2Backend;
let data = vec![0x42u8; CHUNK_SIZE * 4];
let ob = outboard(&backend, &data, BlockSize::ZERO);
assert_eq!(ob.data.len(), 3 * PAIR_SIZE);
}
#[test]
fn outboard_empty() {
let backend = Poseidon2Backend;
let ob = outboard(&backend, &[], BlockSize::ZERO);
assert!(ob.data.is_empty());
assert_eq!(ob.root, backend.chunk_hash(&[], 0, true));
}
#[test]
fn outboard_deterministic() {
let backend = Poseidon2Backend;
let data = vec![0xABu8; 3000];
let ob1 = outboard(&backend, &data, BlockSize::ZERO);
let ob2 = outboard(&backend, &data, BlockSize::ZERO);
assert_eq!(ob1.root, ob2.root);
assert_eq!(ob1.data, ob2.data);
}
#[test]
fn outboard_different_data_different_root() {
let backend = Poseidon2Backend;
let ob1 = outboard(&backend, &[1u8; 2048], BlockSize::ZERO);
let ob2 = outboard(&backend, &[2u8; 2048], BlockSize::ZERO);
assert_ne!(ob1.root, ob2.root);
}
#[test]
fn outboard_partial_last_block() {
let backend = Poseidon2Backend;
let data = vec![0x42u8; CHUNK_SIZE + 500];
let ob = outboard(&backend, &data, BlockSize::ZERO);
assert_eq!(ob.data.len(), PAIR_SIZE);
let left = backend.chunk_hash(&data[..CHUNK_SIZE], 0, false);
let right = backend.chunk_hash(&data[CHUNK_SIZE..], 1, false);
let expected_root = backend.parent_hash(&left, &right, true);
assert_eq!(ob.root, expected_root);
}
}