use crate::{
error::Error,
types::{ArchivedSlashingsDiff, SlashingsDiff, SLOTS_PER_EPOCH},
};
pub fn diff_slashings(
base_slot: u64,
target_slot: u64,
base_buffer: &[u64],
target_buffer: &[u64],
) -> SlashingsDiff {
assert!(
target_slot >= base_slot,
"target_slot must be greater than or equal to base_slot"
);
assert!(!base_buffer.is_empty(), "slashing buffer must not be empty");
assert!(
!target_buffer.is_empty(),
"slashing buffer must not be empty"
);
assert_eq!(
base_buffer.len(),
target_buffer.len(),
"base and target slashing buffers must have the same capacity"
);
let base_epoch = base_slot / SLOTS_PER_EPOCH;
let target_epoch = target_slot / SLOTS_PER_EPOCH;
let capacity = base_buffer.len() as u64;
let mut updates = Vec::new();
let mut current_epoch = base_epoch;
while current_epoch < target_epoch {
current_epoch += 1;
let idx = (current_epoch % capacity) as usize;
let base_value = *base_buffer
.get(idx)
.expect("modulo arithmetic guarantees index is within bounds");
let target_value = *target_buffer
.get(idx)
.expect("modulo arithmetic guarantees index is within bounds");
if base_value != target_value {
let idx_u16 = u16::try_from(idx).expect("slashing buffer capacity exceeds u16");
updates.push((idx_u16, target_value));
}
}
SlashingsDiff { updates }
}
pub fn apply_slashings(
base_buffer: &mut [u64],
delta: &ArchivedSlashingsDiff,
) -> Result<(), Error> {
for update in delta.updates.iter() {
let idx = update.0.to_native() as usize;
let value = update.1.to_native();
let Some(target) = base_buffer.get_mut(idx) else {
return Err(Error::MalformedDelta(format!(
"slashing index {idx} is out of bounds for buffer of length {}",
base_buffer.len()
)));
};
*target = value;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::SlashingsDiff;
fn archive(diff: &SlashingsDiff) -> rkyv::util::AlignedVec {
rkyv::to_bytes::<rkyv::rancor::Error>(diff).expect("test setup: failed to serialize delta")
}
fn archived(bytes: &[u8]) -> &ArchivedSlashingsDiff {
rkyv::access::<ArchivedSlashingsDiff, rkyv::rancor::Error>(bytes)
.expect("test setup: failed to access archived delta")
}
#[test]
fn diff_same_epoch_is_empty() {
let base = vec![0u64; 4];
let target = vec![0u64; 4];
let delta = diff_slashings(0, SLOTS_PER_EPOCH - 1, &base, &target);
assert!(delta.updates.is_empty());
let delta = diff_slashings(SLOTS_PER_EPOCH, 2 * SLOTS_PER_EPOCH - 1, &base, &target);
assert!(delta.updates.is_empty());
}
#[test]
fn diff_single_epoch_transition() {
let base = vec![0u64; 4];
let mut target = vec![0u64; 4];
target[1] = 100;
let delta = diff_slashings(0, SLOTS_PER_EPOCH, &base, &target);
assert_eq!(delta.updates.len(), 1);
assert_eq!(delta.updates[0], (1, 100));
}
#[test]
fn diff_ignores_untouched_indices() {
let base = vec![0u64; 4];
let mut target = vec![0u64; 4];
target[1] = 100; target[2] = 99;
let delta = diff_slashings(0, SLOTS_PER_EPOCH, &base, &target);
assert_eq!(delta.updates.len(), 1);
assert_eq!(delta.updates[0], (1, 100));
}
#[test]
fn diff_wraps_around_capacity() {
let base = vec![0u64; 4];
let mut target = vec![0u64; 4];
target[3] = 10;
target[0] = 20;
target[1] = 30;
target[2] = 40;
let delta = diff_slashings(2 * SLOTS_PER_EPOCH, 6 * SLOTS_PER_EPOCH, &base, &target);
assert_eq!(delta.updates.len(), 4);
assert_eq!(delta.updates[0], (3, 10));
assert_eq!(delta.updates[1], (0, 20));
assert_eq!(delta.updates[2], (1, 30));
assert_eq!(delta.updates[3], (2, 40));
}
#[test]
fn diff_multiple_cycles_produces_redundant_updates() {
let base = vec![0u64; 4];
let mut target = vec![0u64; 4];
target[0] = 99;
target[1] = 88;
target[2] = 77;
target[3] = 66;
let delta = diff_slashings(0, 10 * SLOTS_PER_EPOCH, &base, &target);
assert_eq!(delta.updates.len(), 10);
assert_eq!(delta.updates[0], (1, 88)); assert_eq!(delta.updates[4], (1, 88)); assert_eq!(delta.updates[8], (1, 88)); }
#[test]
fn diff_omits_unchanged_indices() {
let mut base = vec![0u64; 4];
let mut target = vec![0u64; 4];
base[1] = 10;
base[2] = 20;
target[1] = 10; target[2] = 99;
let delta = diff_slashings(0, 2 * SLOTS_PER_EPOCH, &base, &target);
assert_eq!(delta.updates.len(), 1);
assert_eq!(delta.updates[0], (2, 99));
}
#[test]
fn apply_reconstructs_target() {
let base = vec![0u64; 4];
let mut target = vec![0u64; 4];
target[0] = 99;
target[1] = 88;
target[2] = 77;
target[3] = 66;
let delta = diff_slashings(0, 10 * SLOTS_PER_EPOCH, &base, &target);
let bytes = archive(&delta);
let mut reconstructed = base.clone();
apply_slashings(&mut reconstructed, archived(&bytes)).expect("test setup: apply");
assert_eq!(reconstructed, target);
}
#[test]
fn apply_no_op_when_empty() {
let base = vec![0u64; 4];
let target = vec![0u64; 4];
let delta = diff_slashings(0, SLOTS_PER_EPOCH - 1, &base, &target);
let bytes = archive(&delta);
let mut reconstructed = base.clone();
apply_slashings(&mut reconstructed, archived(&bytes)).expect("test setup: apply");
assert_eq!(reconstructed, base);
}
#[test]
#[should_panic(expected = "target_slot must be greater than or equal to base_slot")]
fn diff_panics_on_target_before_base() {
let base = vec![0u64; 4];
let target = vec![0u64; 4];
diff_slashings(SLOTS_PER_EPOCH, 0, &base, &target);
}
#[test]
#[should_panic(expected = "slashing buffer must not be empty")]
fn diff_panics_on_empty_base_buffer() {
diff_slashings(0, SLOTS_PER_EPOCH, &[], &[0u64]);
}
#[test]
#[should_panic(expected = "slashing buffer must not be empty")]
fn diff_panics_on_empty_target_buffer() {
diff_slashings(0, SLOTS_PER_EPOCH, &[0u64], &[]);
}
#[test]
#[should_panic(expected = "base and target slashing buffers must have the same capacity")]
fn diff_panics_on_mismatched_capacity() {
diff_slashings(0, SLOTS_PER_EPOCH, &[0u64; 4], &[0u64; 8]);
}
}