use nebu::{Goldilocks, ntt};
use nebu::field::P;
#[derive(Clone, Debug)]
pub struct Shard {
pub index: usize,
pub data: Vec<Goldilocks>,
}
pub fn encode(data: &[u8], k: usize, n: usize) -> Vec<Shard> {
assert!(n.is_power_of_two(), "n must be power of 2");
assert!(k >= 1 && k <= n, "need 1 <= k <= n");
let elements = bytes_to_elements(data);
let num_groups = (elements.len() + k - 1) / k;
let mut padded = elements;
padded.resize(num_groups * k, Goldilocks::ZERO);
let mut shards: Vec<Shard> = (0..n)
.map(|i| Shard {
index: i,
data: Vec::with_capacity(num_groups),
})
.collect();
for g in 0..num_groups {
let mut poly = vec![Goldilocks::ZERO; n];
for i in 0..k {
poly[i] = padded[g * k + i];
}
ntt::ntt(&mut poly);
for s in 0..n {
shards[s].data.push(poly[s]);
}
}
shards
}
pub fn decode(shards: &[Shard], k: usize, n: usize, original_len: usize) -> Vec<u8> {
assert!(n.is_power_of_two());
assert!(shards.len() >= k, "need at least k shards to reconstruct");
let num_groups = shards[0].data.len();
if shards.len() == n && is_complete(shards, n) {
return decode_full(shards, k, n, num_groups, original_len);
}
let omega = Goldilocks::new(7).exp((P - 1) / n as u64);
let available: Vec<&Shard> = shards.iter().take(k).collect();
let eval_points: Vec<Goldilocks> = available
.iter()
.map(|s| omega.exp(s.index as u64))
.collect();
let mut result_elements = Vec::with_capacity(num_groups * k);
for g in 0..num_groups {
let values: Vec<Goldilocks> = available.iter().map(|s| s.data[g]).collect();
let coeffs = lagrange_interpolate(&eval_points, &values);
for i in 0..k {
result_elements.push(if i < coeffs.len() {
coeffs[i]
} else {
Goldilocks::ZERO
});
}
}
elements_to_bytes(&result_elements, original_len)
}
fn decode_full(
shards: &[Shard],
k: usize,
n: usize,
num_groups: usize,
original_len: usize,
) -> Vec<u8> {
let mut result_elements = Vec::with_capacity(num_groups * k);
for g in 0..num_groups {
let mut poly = vec![Goldilocks::ZERO; n];
for shard in shards {
poly[shard.index] = shard.data[g];
}
ntt::intt(&mut poly);
for i in 0..k {
result_elements.push(poly[i]);
}
}
elements_to_bytes(&result_elements, original_len)
}
fn is_complete(shards: &[Shard], n: usize) -> bool {
if shards.len() != n {
return false;
}
let mut seen = vec![false; n];
for s in shards {
if s.index >= n || seen[s.index] {
return false;
}
seen[s.index] = true;
}
true
}
fn lagrange_interpolate(points: &[Goldilocks], values: &[Goldilocks]) -> Vec<Goldilocks> {
let k = points.len();
assert_eq!(k, values.len());
let mut result = vec![Goldilocks::ZERO; k];
for i in 0..k {
let mut denom = Goldilocks::ONE;
for j in 0..k {
if j != i {
denom = denom * (points[i] - points[j]);
}
}
let scale = values[i] * denom.inv();
let mut basis = vec![Goldilocks::ZERO; k];
basis[0] = Goldilocks::ONE;
let mut deg = 0;
for j in 0..k {
if j != i {
let neg_xj = -points[j];
for d in (1..=deg + 1).rev() {
basis[d] = basis[d - 1] + basis[d] * neg_xj;
}
basis[0] = basis[0] * neg_xj;
deg += 1;
}
}
for d in 0..k {
result[d] = result[d] + scale * basis[d];
}
}
result
}
fn bytes_to_elements(data: &[u8]) -> Vec<Goldilocks> {
let mut out = Vec::with_capacity((data.len() + 6) / 7);
for chunk in data.chunks(7) {
let mut val: u64 = 0;
for (i, &b) in chunk.iter().enumerate() {
val |= (b as u64) << (i * 8);
}
out.push(Goldilocks::new(val));
}
out
}
fn elements_to_bytes(elements: &[Goldilocks], original_len: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(elements.len() * 7);
for &e in elements {
let val = e.as_u64();
for i in 0..7 {
out.push((val >> (i * 8)) as u8);
}
}
out.truncate(original_len);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_all_shards() {
let data = b"hello world, this is a test of erasure coding over Goldilocks!";
let k = 2;
let n = 4;
let shards = encode(data, k, n);
assert_eq!(shards.len(), n);
let recovered = decode(&shards, k, n, data.len());
assert_eq!(&recovered, data);
}
#[test]
fn roundtrip_missing_shards() {
let data = b"erasure coding works: lose any n-k shards and still reconstruct";
let k = 2;
let n = 4;
let shards = encode(data, k, n);
let partial: Vec<Shard> = shards
.into_iter()
.filter(|s| s.index == 0 || s.index == 2)
.collect();
assert_eq!(partial.len(), k);
let recovered = decode(&partial, k, n, data.len());
assert_eq!(&recovered, &data[..]);
}
#[test]
fn roundtrip_k_equals_n() {
let data = b"no parity, full data on every shard";
let k = 4;
let n = 4;
let shards = encode(data, k, n);
let recovered = decode(&shards, k, n, data.len());
assert_eq!(&recovered, data);
}
#[test]
fn roundtrip_large_data() {
let data: Vec<u8> = (0..10_000).map(|i| (i % 256) as u8).collect();
let k = 2;
let n = 4;
let shards = encode(&data, k, n);
let partial: Vec<Shard> = shards
.into_iter()
.filter(|s| s.index == 1 || s.index == 3)
.collect();
let recovered = decode(&partial, k, n, data.len());
assert_eq!(recovered, data);
}
#[test]
fn different_shard_combinations() {
let data = b"testing all possible k-subsets for reconstruction";
let k = 2;
let n = 4;
let shards = encode(data, k, n);
for i in 0..n {
for j in (i + 1)..n {
let partial: Vec<Shard> = shards
.iter()
.filter(|s| s.index == i || s.index == j)
.cloned()
.collect();
let recovered = decode(&partial, k, n, data.len());
assert_eq!(
&recovered,
&data[..],
"failed with shards {i} and {j}"
);
}
}
}
#[test]
fn bytes_roundtrip() {
let data = b"field element encoding roundtrip";
let elems = bytes_to_elements(data);
let back = elements_to_bytes(&elems, data.len());
assert_eq!(&back, &data[..]);
}
}