use crate::{
balances::{read_varint, write_varint},
types::{ArchivedParticipationDiff, ParticipationDiff},
Error,
};
pub fn diff_participation(base: &[u8], target: &[u8]) -> ParticipationDiff {
if target.iter().all(|&value| value == 0) {
return ParticipationDiff::AllZeros(
target
.len()
.try_into()
.expect("target length exceeds u32::MAX"),
);
}
diff_participation_iter(base.iter().copied(), target.iter().copied())
}
pub fn apply_participation(
base: &mut Vec<u8>,
delta: &ArchivedParticipationDiff,
) -> Result<(), Error> {
match delta {
ArchivedParticipationDiff::AllZeros(len) => {
let len = usize::try_from(len.to_native()).map_err(|_| {
Error::InvalidDelta("all-zero participation length does not fit in usize".into())
})?;
base.clear();
base.resize(len, 0);
Ok(())
}
ArchivedParticipationDiff::Sparse { .. } => apply_participation_iter(base, delta),
}
}
pub fn diff_participation_iter<I1, I2>(mut base: I1, mut target: I2) -> ParticipationDiff
where
I1: ExactSizeIterator<Item = u8>,
I2: ExactSizeIterator<Item = u8>,
{
let common_len = base.len().min(target.len());
let mut sparse_indices = Vec::with_capacity(50_000);
let mut new_values = Vec::with_capacity(50_000);
let mut last_idx = 0u64;
for i in 0..common_len {
let Some(v1) = base.next() else {
break;
};
let Some(v2) = target.next() else {
break;
};
if v1 != v2 {
let idx = i as u64;
let gap = idx
.checked_sub(last_idx)
.expect("changed indices are processed in strictly increasing iterator order");
write_varint(gap, &mut sparse_indices);
new_values.push(v2);
last_idx = idx;
}
}
let extension = target.collect();
ParticipationDiff::Sparse {
sparse_indices,
new_values,
extension,
}
}
pub fn apply_participation_iter<T: crate::ListMutTarget<u8>>(
target: &mut T,
delta: &ArchivedParticipationDiff,
) -> Result<(), Error> {
let ArchivedParticipationDiff::Sparse {
sparse_indices,
new_values,
extension,
} = delta
else {
return Err(Error::InvalidDelta(
"AllZeros participation delta cannot be applied through the generic iterator API"
.into(),
));
};
let indices_raw = sparse_indices.as_slice();
let mut cursor = 0usize;
let mut current_idx = 0usize;
for value in new_values.iter() {
let gap = read_varint(indices_raw, &mut cursor)?;
let gap = usize::try_from(gap).map_err(|_| {
Error::MalformedDelta("participation index gap does not fit in usize".into())
})?;
current_idx = current_idx.checked_add(gap).ok_or_else(|| {
Error::MalformedDelta(
"participation index overflow while decoding sparse indices".into(),
)
})?;
let Some(target_value) = target.get_mut(current_idx) else {
return Err(Error::InvalidDelta(format!(
"participation index {current_idx} is outside target collection of length {}",
target.len()
)));
};
*target_value = *value;
}
if cursor != indices_raw.len() {
return Err(Error::InvalidDelta(
"participation sparse index payload contains unused bytes".into(),
));
}
for byte in extension.iter() {
target.push(*byte);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ArchivedParticipationDiff;
use crate::ListMutTarget;
struct MockTarget {
inner: Vec<u8>,
}
impl ListMutTarget<u8> for MockTarget {
fn len(&self) -> usize {
self.inner.len()
}
fn get_mut(&mut self, index: usize) -> Option<&mut u8> {
self.inner.get_mut(index)
}
fn push(&mut self, value: u8) {
self.inner.push(value);
}
}
fn assert_sparse_roundtrip(base: &[u8], target: &[u8]) {
let delta = diff_participation(base, target);
match &delta {
ParticipationDiff::AllZeros(_) => panic!("test setup: expected sparse delta"),
ParticipationDiff::Sparse { .. } => {}
}
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut reconstructed = base.to_vec();
apply_participation(&mut reconstructed, archived).expect("test setup: apply");
assert_eq!(reconstructed, target);
}
#[test]
fn test_diff_slice_no_changes() {
let base = vec![1, 2, 3];
let target = vec![1, 2, 3];
let delta = diff_participation(&base, &target);
match delta {
ParticipationDiff::Sparse {
sparse_indices,
new_values,
extension,
} => {
assert!(sparse_indices.is_empty());
assert!(new_values.is_empty());
assert!(extension.is_empty());
}
_ => panic!("test setup: expected sparse"),
}
}
#[test]
fn test_diff_slice_all_zeros_fast_path() {
let base = vec![1, 2, 3];
let target = vec![0, 0, 0];
let delta = diff_participation(&base, &target);
match delta {
ParticipationDiff::AllZeros(len) => assert_eq!(len, 3),
_ => panic!("expected AllZeros fast path"),
}
}
#[test]
fn test_apply_all_zeros() {
let mut base = vec![1, 2, 3];
let delta = ParticipationDiff::AllZeros(5);
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
apply_participation(&mut base, archived).expect("test setup: apply");
assert_eq!(base, vec![0, 0, 0, 0, 0]);
}
#[test]
fn test_diff_iter_all_zeros_produces_sparse() {
let base = vec![1, 2, 3];
let target = vec![0, 0, 0];
let delta = diff_participation_iter(base.into_iter(), target.into_iter());
match delta {
ParticipationDiff::Sparse { .. } => {} _ => panic!("iterator API must produce sparse, not AllZeros"),
}
}
#[test]
fn test_sparse_roundtrip_with_changes() {
let base = vec![0, 0, 0, 0];
let target = vec![1, 0, 2, 0];
assert_sparse_roundtrip(&base, &target);
}
#[test]
fn test_sparse_roundtrip_with_appended() {
let base = vec![0];
let target = vec![0, 5, 6];
assert_sparse_roundtrip(&base, &target);
}
#[test]
fn test_sparse_roundtrip_combined() {
let base = vec![10, 20, 30];
let target = vec![10, 99, 30, 40, 50]; assert_sparse_roundtrip(&base, &target);
}
#[test]
fn test_apply_all_zeros_via_iter_api_errors() {
let delta = ParticipationDiff::AllZeros(5);
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut target = MockTarget { inner: vec![] };
let result = apply_participation_iter(&mut target, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("AllZeros participation delta cannot be applied"));
}
#[test]
fn test_apply_sparse_mismatched_counts() {
let delta = ParticipationDiff::Sparse {
sparse_indices: vec![0], new_values: vec![],
extension: vec![],
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut target = MockTarget { inner: vec![0] };
let result = apply_participation_iter(&mut target, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("unused bytes"));
}
#[test]
fn test_apply_sparse_index_out_of_bounds() {
let delta = ParticipationDiff::Sparse {
sparse_indices: vec![5],
new_values: vec![99],
extension: vec![],
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut target = MockTarget { inner: vec![0; 3] };
let result = apply_participation_iter(&mut target, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("outside target collection"));
}
#[test]
fn test_apply_sparse_truncated_varint() {
let delta = ParticipationDiff::Sparse {
sparse_indices: vec![0xFF], new_values: vec![0],
extension: vec![],
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut target = MockTarget { inner: vec![0] };
let result = apply_participation_iter(&mut target, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("truncated varint"));
}
#[test]
fn test_apply_sparse_index_sum_overflow() {
let sparse_indices: Vec<u8> = vec![
0xE7, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01, ];
let delta = ParticipationDiff::Sparse {
sparse_indices,
new_values: vec![0, 0],
extension: vec![],
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta).expect("test setup: serialize");
let archived = rkyv::access::<ArchivedParticipationDiff, rkyv::rancor::Error>(&bytes)
.expect("test setup: failed to access archived delta");
let mut target = MockTarget {
inner: vec![0; 1000],
};
let result = apply_participation_iter(&mut target, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("index overflow"));
}
}