use crate::error::{Result, StorageError};
use reed_solomon_erasure::galois_8::ReedSolomon;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use tenzro_types::primitives::Hash;
const LEAF_TAG: &[u8] = b"tenzro/redstuff/sliver";
const NODE_TAG: &[u8] = b"tenzro/redstuff/node";
const BLOB_TAG: &[u8] = b"tenzro/redstuff/blob";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CommitteeShape {
pub f: usize,
}
impl CommitteeShape {
pub fn from_fault_bound(f: usize) -> Result<Self> {
if f == 0 {
return Err(StorageError::InvalidValue(
"Red Stuff fault bound f must be >= 1".into(),
));
}
Ok(Self { f })
}
pub fn from_committee_size(n: usize) -> Result<Self> {
if n < 4 {
return Err(StorageError::InvalidValue(format!(
"Red Stuff committee needs n >= 4, got {n}"
)));
}
Self::from_fault_bound((n - 1) / 3)
}
pub fn n(&self) -> usize {
3 * self.f + 1
}
pub fn rows(&self) -> usize {
self.f + 1
}
pub fn cols(&self) -> usize {
2 * self.f + 1
}
pub fn quorum(&self) -> usize {
2 * self.f + 1
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SliverPair {
pub node_index: usize,
pub primary: Vec<u8>,
pub secondary: Vec<u8>,
pub primary_proof: Vec<Hash>,
pub secondary_proof: Vec<Hash>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EncodedBlob {
pub shape: CommitteeShape,
pub blob_len: u64,
pub symbol_len: usize,
pub commitment: Hash,
pub slivers: Vec<SliverPair>,
}
impl EncodedBlob {
pub fn blob_id(&self) -> Hash {
self.commitment
}
}
fn hash_leaf(kind: u8, node_index: usize, symbols: &[u8]) -> Hash {
let mut h = Sha256::new();
h.update(LEAF_TAG);
h.update([kind]);
h.update((node_index as u64).to_le_bytes());
h.update(symbols);
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
Hash::new(out)
}
fn hash_node(left: &Hash, right: &Hash) -> Hash {
let mut h = Sha256::new();
h.update(NODE_TAG);
h.update(left.as_bytes());
h.update(right.as_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
Hash::new(out)
}
fn merkle_tree(leaves: &[Hash]) -> (Hash, Vec<Vec<Hash>>) {
if leaves.is_empty() {
return (Hash::new([0u8; 32]), Vec::new());
}
let mut proofs: Vec<Vec<Hash>> = vec![Vec::new(); leaves.len()];
let mut positions: Vec<usize> = (0..leaves.len()).collect();
let mut level: Vec<Hash> = leaves.to_vec();
while level.len() > 1 {
let mut next = Vec::with_capacity(level.len().div_ceil(2));
for pair in level.chunks(2) {
let left = pair[0];
let right = if pair.len() == 2 { pair[1] } else { pair[0] };
next.push(hash_node(&left, &right));
}
for (leaf, pos) in proofs.iter_mut().zip(positions.iter_mut()) {
let sibling = if *pos % 2 == 0 {
let s = *pos + 1;
if s < level.len() {
level[s]
} else {
level[*pos]
}
} else {
level[*pos - 1]
};
leaf.push(sibling);
*pos /= 2;
}
level = next;
}
(level[0], proofs)
}
fn merkle_root_from_proof(leaf: Hash, mut index: usize, proof: &[Hash]) -> Hash {
let mut acc = leaf;
for sibling in proof {
acc = if index.is_multiple_of(2) {
hash_node(&acc, sibling)
} else {
hash_node(sibling, &acc)
};
index /= 2;
}
acc
}
fn blob_commitment(
shape: CommitteeShape,
blob_len: u64,
symbol_len: usize,
sliver_root: &Hash,
) -> Hash {
let mut h = Sha256::new();
h.update(BLOB_TAG);
h.update((shape.f as u64).to_le_bytes());
h.update(blob_len.to_le_bytes());
h.update((symbol_len as u64).to_le_bytes());
h.update(sliver_root.as_bytes());
let mut out = [0u8; 32];
out.copy_from_slice(&h.finalize());
Hash::new(out)
}
pub fn encode(data: &[u8], shape: CommitteeShape) -> Result<EncodedBlob> {
let rows = shape.rows();
let cols = shape.cols();
let n = shape.n();
let source_symbols = rows * cols;
let symbol_len = data.len().div_ceil(source_symbols).max(1);
let mut matrix = vec![vec![0u8; symbol_len]; source_symbols];
for (i, chunk) in data.chunks(symbol_len).enumerate() {
matrix[i][..chunk.len()].copy_from_slice(chunk);
}
let at = |r: usize, c: usize| &matrix[r * cols + c];
let primary_rs = ReedSolomon::new(rows, n - rows)
.map_err(|e| StorageError::Generic(format!("primary RS init: {e}")))?;
let mut primary_ext: Vec<Vec<Vec<u8>>> = Vec::with_capacity(cols); for c in 0..cols {
let mut shards: Vec<Vec<u8>> = Vec::with_capacity(n);
for r in 0..rows {
shards.push(at(r, c).clone());
}
for _ in rows..n {
shards.push(vec![0u8; symbol_len]);
}
primary_rs
.encode(&mut shards)
.map_err(|e| StorageError::Generic(format!("primary encode col {c}: {e}")))?;
primary_ext.push(shards);
}
let mut primary_rows: Vec<Vec<u8>> = vec![Vec::with_capacity(cols * symbol_len); n];
for node_symbols in primary_ext.iter() {
for (node, sym) in node_symbols.iter().enumerate() {
primary_rows[node].extend_from_slice(sym);
}
}
let secondary_rs = ReedSolomon::new(cols, n - cols)
.map_err(|e| StorageError::Generic(format!("secondary RS init: {e}")))?;
let mut secondary_ext: Vec<Vec<Vec<u8>>> = Vec::with_capacity(rows); for r in 0..rows {
let mut shards: Vec<Vec<u8>> = Vec::with_capacity(n);
for c in 0..cols {
shards.push(at(r, c).clone());
}
for _ in cols..n {
shards.push(vec![0u8; symbol_len]);
}
secondary_rs
.encode(&mut shards)
.map_err(|e| StorageError::Generic(format!("secondary encode row {r}: {e}")))?;
secondary_ext.push(shards);
}
let mut secondary_cols: Vec<Vec<u8>> = vec![Vec::with_capacity(rows * symbol_len); n];
for node_symbols in secondary_ext.iter() {
for (node, sym) in node_symbols.iter().enumerate() {
secondary_cols[node].extend_from_slice(sym);
}
}
let mut leaves = Vec::with_capacity(2 * n);
for (node, primary) in primary_rows.iter().enumerate() {
leaves.push(hash_leaf(0, node, primary));
}
for (node, secondary) in secondary_cols.iter().enumerate() {
leaves.push(hash_leaf(1, node, secondary));
}
let (sliver_root, proofs) = merkle_tree(&leaves);
let commitment = blob_commitment(shape, data.len() as u64, symbol_len, &sliver_root);
let mut slivers = Vec::with_capacity(n);
for node in 0..n {
slivers.push(SliverPair {
node_index: node,
primary: std::mem::take(&mut primary_rows[node]),
secondary: std::mem::take(&mut secondary_cols[node]),
primary_proof: proofs[node].clone(),
secondary_proof: proofs[n + node].clone(),
});
}
Ok(EncodedBlob {
shape,
blob_len: data.len() as u64,
symbol_len,
commitment,
slivers,
})
}
pub fn verify_sliver(
sliver: &SliverPair,
shape: CommitteeShape,
blob_len: u64,
symbol_len: usize,
commitment: &Hash,
) -> bool {
let n = shape.n();
if sliver.node_index >= n {
return false;
}
if sliver.primary.len() != shape.cols() * symbol_len
|| sliver.secondary.len() != shape.rows() * symbol_len
{
return false;
}
let primary_leaf = hash_leaf(0, sliver.node_index, &sliver.primary);
let secondary_leaf = hash_leaf(1, sliver.node_index, &sliver.secondary);
let primary_root =
merkle_root_from_proof(primary_leaf, sliver.node_index, &sliver.primary_proof);
let secondary_root = merkle_root_from_proof(
secondary_leaf,
n + sliver.node_index,
&sliver.secondary_proof,
);
if primary_root != secondary_root {
return false;
}
&blob_commitment(shape, blob_len, symbol_len, &primary_root) == commitment
}
pub fn reconstruct(
slivers: &[SliverPair],
shape: CommitteeShape,
blob_len: u64,
symbol_len: usize,
commitment: &Hash,
) -> Result<Vec<u8>> {
let rows = shape.rows();
let cols = shape.cols();
let n = shape.n();
let mut present: Vec<Option<&SliverPair>> = vec![None; n];
let mut valid = 0usize;
for s in slivers {
if s.node_index >= n || present[s.node_index].is_some() {
continue;
}
if verify_sliver(s, shape, blob_len, symbol_len, commitment) {
present[s.node_index] = Some(s);
valid += 1;
}
}
if valid < cols {
return Err(StorageError::InvalidValue(format!(
"Red Stuff reconstruct needs {cols} valid secondary slivers, have {valid}"
)));
}
let secondary_rs = ReedSolomon::new(cols, n - cols)
.map_err(|e| StorageError::Generic(format!("secondary RS init: {e}")))?;
let mut matrix = vec![vec![0u8; symbol_len]; rows * cols];
for r in 0..rows {
let mut shards: Vec<Option<Vec<u8>>> = vec![None; n];
for (node, slot) in present.iter().enumerate() {
if let Some(s) = slot {
let start = r * symbol_len;
shards[node] = Some(s.secondary[start..start + symbol_len].to_vec());
}
}
secondary_rs
.reconstruct(&mut shards)
.map_err(|e| StorageError::Generic(format!("secondary reconstruct row {r}: {e}")))?;
for c in 0..cols {
let sym = shards[c]
.as_ref()
.ok_or_else(|| StorageError::Generic("row symbol missing after decode".into()))?;
matrix[r * cols + c].copy_from_slice(sym);
}
}
let mut out = Vec::with_capacity(rows * cols * symbol_len);
for sym in &matrix {
out.extend_from_slice(sym);
}
out.truncate(blob_len as usize);
let re = encode(&out, shape)?;
if &re.commitment != commitment {
return Err(StorageError::InvalidValue(
"Red Stuff reconstruct: re-encoded commitment does not match".into(),
));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shape_derivations() {
let s = CommitteeShape::from_fault_bound(1).unwrap();
assert_eq!(s.n(), 4);
assert_eq!(s.rows(), 2);
assert_eq!(s.cols(), 3);
assert_eq!(s.quorum(), 3);
let s10 = CommitteeShape::from_committee_size(10).unwrap();
assert_eq!(s10.f, 3);
assert_eq!(s10.n(), 10);
assert_eq!(s10.quorum(), 7);
assert!(CommitteeShape::from_fault_bound(0).is_err());
assert!(CommitteeShape::from_committee_size(3).is_err());
}
#[test]
fn encode_produces_n_sliver_pairs() {
let shape = CommitteeShape::from_fault_bound(2).unwrap(); let data = vec![42u8; 5000];
let enc = encode(&data, shape).unwrap();
assert_eq!(enc.slivers.len(), 7);
assert_eq!(enc.blob_len, 5000);
for s in &enc.slivers {
assert_eq!(s.primary.len(), shape.cols() * enc.symbol_len);
assert_eq!(s.secondary.len(), shape.rows() * enc.symbol_len);
}
}
#[test]
fn every_sliver_binds_to_commitment() {
let shape = CommitteeShape::from_fault_bound(2).unwrap();
let data = b"red stuff two-dimensional erasure encoding round trip".to_vec();
let enc = encode(&data, shape).unwrap();
for s in &enc.slivers {
assert!(verify_sliver(
s,
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment
));
}
}
#[test]
fn reconstruct_from_exact_quorum() {
let shape = CommitteeShape::from_fault_bound(2).unwrap(); let data = vec![9u8; 4096];
let enc = encode(&data, shape).unwrap();
let subset: Vec<SliverPair> = enc.slivers.iter().take(shape.cols()).cloned().collect();
let back = reconstruct(
&subset,
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment,
)
.unwrap();
assert_eq!(back, data);
}
#[test]
fn reconstruct_tolerates_f_missing() {
let shape = CommitteeShape::from_fault_bound(3).unwrap(); let data: Vec<u8> = (0..8000u32).map(|i| (i % 251) as u8).collect();
let enc = encode(&data, shape).unwrap();
let subset: Vec<SliverPair> = enc
.slivers
.iter()
.filter(|s| s.node_index >= 3)
.cloned()
.collect();
assert_eq!(subset.len(), 7);
let back = reconstruct(
&subset,
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment,
)
.unwrap();
assert_eq!(back, data);
}
#[test]
fn reconstruct_fails_below_quorum() {
let shape = CommitteeShape::from_fault_bound(2).unwrap(); let data = vec![1u8; 1000];
let enc = encode(&data, shape).unwrap();
let subset: Vec<SliverPair> = enc.slivers.iter().take(4).cloned().collect();
assert!(
reconstruct(
&subset,
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment
)
.is_err()
);
}
#[test]
fn forged_sliver_is_discarded() {
let shape = CommitteeShape::from_fault_bound(2).unwrap();
let data = vec![7u8; 3000];
let enc = encode(&data, shape).unwrap();
let mut tampered = enc.slivers.clone();
tampered[0].secondary[0] ^= 0xFF;
assert!(!verify_sliver(
&tampered[0],
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment
));
let back = reconstruct(
&tampered,
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment,
)
.unwrap();
assert_eq!(back, data);
}
#[test]
fn wrong_commitment_rejects_all() {
let shape = CommitteeShape::from_fault_bound(1).unwrap();
let data = vec![5u8; 256];
let enc = encode(&data, shape).unwrap();
let bogus = Hash::new([0xAAu8; 32]);
for s in &enc.slivers {
assert!(!verify_sliver(
s,
shape,
enc.blob_len,
enc.symbol_len,
&bogus
));
}
assert!(reconstruct(&enc.slivers, shape, enc.blob_len, enc.symbol_len, &bogus).is_err());
}
#[test]
fn empty_and_small_blobs_round_trip() {
let shape = CommitteeShape::from_fault_bound(1).unwrap();
for data in [Vec::new(), vec![1u8], b"hi".to_vec()] {
let enc = encode(&data, shape).unwrap();
let subset: Vec<SliverPair> = enc.slivers.iter().take(shape.cols()).cloned().collect();
let back = reconstruct(
&subset,
shape,
enc.blob_len,
enc.symbol_len,
&enc.commitment,
)
.unwrap();
assert_eq!(back, data);
}
}
}