use crate::{
error::Error,
types::{
ArchivedValidatorField, ArchivedValidatorsDiff, ValidatorField, ValidatorPatch,
ValidatorsDiff, MIN_VALIDATOR_WITHDRAWABILITY_DELAY, VALIDATOR_SSZ_SIZE,
},
};
pub trait ValidatorSnapshot {
fn withdrawal_credentials(&self) -> &[u8; 32];
fn effective_balance(&self) -> u64;
fn is_slashed(&self) -> bool;
fn activation_eligibility_epoch(&self) -> u64;
fn activation_epoch(&self) -> u64;
fn exit_epoch(&self) -> u64;
fn withdrawable_epoch(&self) -> u64;
fn to_ssz_bytes(&self) -> Vec<u8>;
}
pub trait ValidatorMut {
fn is_slashed(&self) -> bool;
fn set_withdrawal_credentials(&mut self, value: &[u8; 32]);
fn set_effective_balance(&mut self, value: u64);
fn set_slashed(&mut self, value: bool);
fn set_activation_eligibility_epoch(&mut self, value: u64);
fn set_activation_epoch(&mut self, value: u64);
fn set_exit_epoch(&mut self, value: u64);
fn set_withdrawable_epoch(&mut self, value: u64);
}
pub trait ValidatorMutTarget {
type Validator<'a>: ValidatorMut
where
Self: 'a;
fn get_mut(&mut self, index: usize) -> Option<Self::Validator<'_>>;
fn push_from_ssz(&mut self, ssz_bytes: &[u8]);
}
pub fn diff_validators(base_bytes: &[u8], target_bytes: &[u8]) -> ValidatorsDiff {
diff_validators_impl(
base_bytes
.chunks_exact(VALIDATOR_SSZ_SIZE)
.map(ByteValidator::new),
target_bytes
.chunks_exact(VALIDATOR_SSZ_SIZE)
.map(ByteValidator::new),
)
}
pub fn diff_validators_iter<I1, I2, V1, V2>(base: I1, target: I2) -> ValidatorsDiff
where
I1: ExactSizeIterator<Item = V1>,
I2: ExactSizeIterator<Item = V2>,
V1: ValidatorSnapshot,
V2: ValidatorSnapshot,
{
diff_validators_impl(base, target)
}
fn diff_validators_impl<I1, I2, V1, V2>(mut base: I1, mut target: I2) -> ValidatorsDiff
where
I1: ExactSizeIterator<Item = V1>,
I2: ExactSizeIterator<Item = V2>,
V1: ValidatorSnapshot,
V2: ValidatorSnapshot,
{
let mut patches = Vec::with_capacity(512);
let mut appended_validators = Vec::new();
for (i, (b, t)) in base.by_ref().zip(target.by_ref()).enumerate() {
let wc = t.withdrawal_credentials();
let eb = t.effective_balance();
let slashed = t.is_slashed();
let aee = t.activation_eligibility_epoch();
let ae = t.activation_epoch();
let ee = t.exit_epoch();
if b.withdrawal_credentials() == wc
&& b.effective_balance() == eb
&& b.is_slashed() == slashed
&& b.activation_eligibility_epoch() == aee
&& b.activation_epoch() == ae
&& b.exit_epoch() == ee
&& (!slashed || b.withdrawable_epoch() == t.withdrawable_epoch())
{
continue;
}
let index = u32::try_from(i).expect("validator index exceeds u32 range");
if b.withdrawal_credentials() != wc {
patches.push(ValidatorPatch {
index,
field: ValidatorField::WithdrawalCredentials,
value: wc.to_vec(),
});
}
if b.effective_balance() != eb {
patches.push(ValidatorPatch {
index,
field: ValidatorField::EffectiveBalance,
value: eb.to_le_bytes().to_vec(),
});
}
if b.is_slashed() != slashed {
patches.push(ValidatorPatch {
index,
field: ValidatorField::Slashed,
value: vec![slashed as u8],
});
}
if b.activation_eligibility_epoch() != aee {
patches.push(ValidatorPatch {
index,
field: ValidatorField::ActivationEligibilityEpoch,
value: aee.to_le_bytes().to_vec(),
});
}
if b.activation_epoch() != ae {
patches.push(ValidatorPatch {
index,
field: ValidatorField::ActivationEpoch,
value: ae.to_le_bytes().to_vec(),
});
}
if b.exit_epoch() != ee {
patches.push(ValidatorPatch {
index,
field: ValidatorField::ExitEpoch,
value: ee.to_le_bytes().to_vec(),
});
}
if slashed && b.withdrawable_epoch() != t.withdrawable_epoch() {
patches.push(ValidatorPatch {
index,
field: ValidatorField::WithdrawableEpochSlashed,
value: t.withdrawable_epoch().to_le_bytes().to_vec(),
});
}
}
for t_val in target {
appended_validators.extend(t_val.to_ssz_bytes());
}
ValidatorsDiff {
patches,
appended_validators,
}
}
pub fn apply_validators(base: &mut Vec<u8>, delta: &ArchivedValidatorsDiff) -> Result<(), Error> {
apply_validators_iter(&mut ByteValidatorTarget(base), delta)
}
pub fn apply_validators_iter<T: ValidatorMutTarget>(
target: &mut T,
delta: &ArchivedValidatorsDiff,
) -> Result<(), Error> {
for patch in delta.patches.iter() {
let idx = patch.index.to_native() as usize;
let val_bytes = patch.value.as_slice();
let mut validator = target.get_mut(idx).ok_or_else(|| {
Error::InvalidDelta(format!("validator patch index {idx} is out of bounds"))
})?;
match &patch.field {
ArchivedValidatorField::WithdrawalCredentials => {
let bytes: [u8; 32] = val_bytes.try_into().map_err(|_| {
Error::MalformedDelta(format!(
"withdrawal credentials patch has invalid width: \
expected 32 bytes, got {}",
val_bytes.len()
))
})?;
validator.set_withdrawal_credentials(&bytes);
}
ArchivedValidatorField::EffectiveBalance => {
let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
Error::MalformedDelta(format!(
"effective balance patch has invalid width: \
expected 8 bytes, got {}",
val_bytes.len()
))
})?;
let eb = u64::from_le_bytes(bytes);
validator.set_effective_balance(eb);
}
ArchivedValidatorField::Slashed => {
let [value] = <[u8; 1]>::try_from(val_bytes).map_err(|_| {
Error::MalformedDelta(format!(
"slashed patch has invalid width: expected 1 byte, got {}",
val_bytes.len()
))
})?;
validator.set_slashed(value != 0);
}
ArchivedValidatorField::ActivationEligibilityEpoch => {
let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
Error::MalformedDelta(format!(
"activation eligibility epoch patch has invalid width: \
expected 8 bytes, got {}",
val_bytes.len()
))
})?;
let epoch = u64::from_le_bytes(bytes);
validator.set_activation_eligibility_epoch(epoch);
}
ArchivedValidatorField::ActivationEpoch => {
let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
Error::MalformedDelta(format!(
"activation epoch patch has invalid width: \
expected 8 bytes, got {}",
val_bytes.len()
))
})?;
let epoch = u64::from_le_bytes(bytes);
validator.set_activation_epoch(epoch);
}
ArchivedValidatorField::ExitEpoch => {
let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
Error::MalformedDelta(format!(
"exit epoch patch has invalid width: \
expected 8 bytes, got {}",
val_bytes.len()
))
})?;
let ee = u64::from_le_bytes(bytes);
validator.set_exit_epoch(ee);
if !validator.is_slashed() {
let we = ee.saturating_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY);
validator.set_withdrawable_epoch(we);
}
}
ArchivedValidatorField::WithdrawableEpochSlashed => {
let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
Error::MalformedDelta(format!(
"withdrawable epoch patch has invalid width: \
expected 8 bytes, got {}",
val_bytes.len()
))
})?;
let we = u64::from_le_bytes(bytes);
validator.set_withdrawable_epoch(we);
}
}
}
if delta.appended_validators.len() % VALIDATOR_SSZ_SIZE != 0 {
return Err(Error::MalformedDelta(
"appended validator data does not contain complete SSZ records".into(),
));
}
for chunk in delta
.appended_validators
.as_slice()
.chunks_exact(VALIDATOR_SSZ_SIZE)
{
target.push_from_ssz(chunk);
}
Ok(())
}
struct ByteValidator<'a>(&'a [u8]);
impl<'a> ByteValidator<'a> {
fn new(bytes: &'a [u8]) -> Self {
debug_assert_eq!(
bytes.len(),
VALIDATOR_SSZ_SIZE,
"ByteValidator must contain exactly one complete SSZ validator record",
);
Self(bytes)
}
#[inline]
fn bytes<const N: usize>(&self, start: usize) -> [u8; N] {
self.0
.get(start..start + N)
.and_then(|bytes| bytes.try_into().ok())
.expect("ByteValidator contains a complete SSZ validator record")
}
}
impl<'a> ValidatorSnapshot for ByteValidator<'a> {
#[inline]
fn withdrawal_credentials(&self) -> &[u8; 32] {
self.0
.get(48..80)
.and_then(|bytes| bytes.try_into().ok())
.expect("ByteValidator contains a complete SSZ validator record")
}
#[inline]
fn effective_balance(&self) -> u64 {
u64::from_le_bytes(self.bytes::<8>(80))
}
#[inline]
fn is_slashed(&self) -> bool {
self.bytes::<1>(88)[0] != 0
}
#[inline]
fn activation_eligibility_epoch(&self) -> u64 {
u64::from_le_bytes(self.bytes::<8>(89))
}
#[inline]
fn activation_epoch(&self) -> u64 {
u64::from_le_bytes(self.bytes::<8>(97))
}
#[inline]
fn exit_epoch(&self) -> u64 {
u64::from_le_bytes(self.bytes::<8>(105))
}
#[inline]
fn withdrawable_epoch(&self) -> u64 {
u64::from_le_bytes(self.bytes::<8>(113))
}
#[inline]
fn to_ssz_bytes(&self) -> Vec<u8> {
self.0.to_vec()
}
}
struct ByteValidatorMut<'a>(&'a mut [u8]);
impl<'a> ValidatorMut for ByteValidatorMut<'a> {
#[inline]
fn is_slashed(&self) -> bool {
*self
.0
.get(88)
.expect("ByteValidatorMut contains a complete SSZ validator record")
!= 0
}
#[inline]
fn set_withdrawal_credentials(&mut self, v: &[u8; 32]) {
self.0
.get_mut(48..80)
.expect("ByteValidatorMut contains a complete SSZ validator record")
.copy_from_slice(v);
}
#[inline]
fn set_effective_balance(&mut self, v: u64) {
self.0
.get_mut(80..88)
.expect("ByteValidatorMut contains a complete SSZ validator record")
.copy_from_slice(&v.to_le_bytes());
}
#[inline]
fn set_slashed(&mut self, v: bool) {
*self
.0
.get_mut(88)
.expect("ByteValidatorMut contains a complete SSZ validator record") = v as u8;
}
#[inline]
fn set_activation_eligibility_epoch(&mut self, v: u64) {
self.0
.get_mut(89..97)
.expect("ByteValidatorMut contains a complete SSZ validator record")
.copy_from_slice(&v.to_le_bytes());
}
#[inline]
fn set_activation_epoch(&mut self, v: u64) {
self.0
.get_mut(97..105)
.expect("ByteValidatorMut contains a complete SSZ validator record")
.copy_from_slice(&v.to_le_bytes());
}
#[inline]
fn set_exit_epoch(&mut self, v: u64) {
self.0
.get_mut(105..113)
.expect("ByteValidatorMut contains a complete SSZ validator record")
.copy_from_slice(&v.to_le_bytes());
}
#[inline]
fn set_withdrawable_epoch(&mut self, v: u64) {
self.0
.get_mut(113..121)
.expect("ByteValidatorMut contains a complete SSZ validator record")
.copy_from_slice(&v.to_le_bytes());
}
}
struct ByteValidatorTarget<'a>(&'a mut Vec<u8>);
impl<'a> ValidatorMutTarget for ByteValidatorTarget<'a> {
type Validator<'b>
= ByteValidatorMut<'b>
where
Self: 'b;
fn get_mut(&mut self, index: usize) -> Option<Self::Validator<'_>> {
let start = index.checked_mul(VALIDATOR_SSZ_SIZE)?;
let end = start.checked_add(VALIDATOR_SSZ_SIZE)?;
self.0.get_mut(start..end).map(ByteValidatorMut)
}
fn push_from_ssz(&mut self, ssz_bytes: &[u8]) {
self.0.extend_from_slice(ssz_bytes);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{ArchivedValidatorsDiff, ValidatorField};
fn archive(diff: &ValidatorsDiff) -> rkyv::util::AlignedVec {
rkyv::to_bytes::<rkyv::rancor::Error>(diff).expect("test setup: failed to serialize delta")
}
fn archived(bytes: &[u8]) -> &ArchivedValidatorsDiff {
rkyv::access::<ArchivedValidatorsDiff, rkyv::rancor::Error>(bytes)
.expect("test setup: failed to access archived delta")
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct MockValidator {
withdrawal_credentials: [u8; 32],
effective_balance: u64,
slashed: bool,
activation_eligibility_epoch: u64,
activation_epoch: u64,
exit_epoch: u64,
withdrawable_epoch: u64,
}
impl MockValidator {
fn new(id: u8) -> Self {
Self {
withdrawal_credentials: [id; 32],
effective_balance: 32_000_000_000,
slashed: false,
activation_eligibility_epoch: 0,
activation_epoch: 0,
exit_epoch: 0,
withdrawable_epoch: 0,
}
}
fn to_ssz_bytes(&self) -> Vec<u8> {
let mut bytes = vec![0u8; VALIDATOR_SSZ_SIZE];
bytes[48..80].copy_from_slice(&self.withdrawal_credentials);
bytes[80..88].copy_from_slice(&self.effective_balance.to_le_bytes());
bytes[88] = self.slashed as u8;
bytes[89..97].copy_from_slice(&self.activation_eligibility_epoch.to_le_bytes());
bytes[97..105].copy_from_slice(&self.activation_epoch.to_le_bytes());
bytes[105..113].copy_from_slice(&self.exit_epoch.to_le_bytes());
bytes[113..121].copy_from_slice(&self.withdrawable_epoch.to_le_bytes());
bytes
}
}
impl ValidatorSnapshot for MockValidator {
fn withdrawal_credentials(&self) -> &[u8; 32] {
&self.withdrawal_credentials
}
fn effective_balance(&self) -> u64 {
self.effective_balance
}
fn is_slashed(&self) -> bool {
self.slashed
}
fn activation_eligibility_epoch(&self) -> u64 {
self.activation_eligibility_epoch
}
fn activation_epoch(&self) -> u64 {
self.activation_epoch
}
fn exit_epoch(&self) -> u64 {
self.exit_epoch
}
fn withdrawable_epoch(&self) -> u64 {
self.withdrawable_epoch
}
fn to_ssz_bytes(&self) -> Vec<u8> {
self.to_ssz_bytes()
}
}
struct MockMutValidator<'a>(&'a mut MockValidator);
impl<'a> ValidatorMut for MockMutValidator<'a> {
fn is_slashed(&self) -> bool {
self.0.slashed
}
fn set_withdrawal_credentials(&mut self, v: &[u8; 32]) {
self.0.withdrawal_credentials = *v;
}
fn set_effective_balance(&mut self, v: u64) {
self.0.effective_balance = v;
}
fn set_slashed(&mut self, v: bool) {
self.0.slashed = v;
}
fn set_activation_eligibility_epoch(&mut self, v: u64) {
self.0.activation_eligibility_epoch = v;
}
fn set_activation_epoch(&mut self, v: u64) {
self.0.activation_epoch = v;
}
fn set_exit_epoch(&mut self, v: u64) {
self.0.exit_epoch = v;
}
fn set_withdrawable_epoch(&mut self, v: u64) {
self.0.withdrawable_epoch = v;
}
}
struct MockValidatorTarget {
validators: Vec<MockValidator>,
}
impl ValidatorMutTarget for MockValidatorTarget {
type Validator<'a>
= MockMutValidator<'a>
where
Self: 'a;
fn get_mut(&mut self, index: usize) -> Option<Self::Validator<'_>> {
self.validators.get_mut(index).map(MockMutValidator)
}
fn push_from_ssz(&mut self, ssz_bytes: &[u8]) {
let wc = ssz_bytes
.get(48..80)
.and_then(|s| s.try_into().ok())
.expect("test setup: valid wc bytes");
let eb = u64::from_le_bytes(
ssz_bytes
.get(80..88)
.and_then(|s| s.try_into().ok())
.expect("test setup: valid eb bytes"),
);
let slashed = ssz_bytes.get(88).copied().expect("test setup") != 0;
let aee = u64::from_le_bytes(
ssz_bytes
.get(89..97)
.and_then(|s| s.try_into().ok())
.expect("test setup: valid aee bytes"),
);
let ae = u64::from_le_bytes(
ssz_bytes
.get(97..105)
.and_then(|s| s.try_into().ok())
.expect("test setup: valid ae bytes"),
);
let ee = u64::from_le_bytes(
ssz_bytes
.get(105..113)
.and_then(|s| s.try_into().ok())
.expect("test setup: valid ee bytes"),
);
let we = u64::from_le_bytes(
ssz_bytes
.get(113..121)
.and_then(|s| s.try_into().ok())
.expect("test setup: valid we bytes"),
);
self.validators.push(MockValidator {
withdrawal_credentials: wc,
effective_balance: eb,
slashed,
activation_eligibility_epoch: aee,
activation_epoch: ae,
exit_epoch: ee,
withdrawable_epoch: we,
});
}
}
#[test]
fn test_no_changes_iter() {
let base = vec![MockValidator::new(0), MockValidator::new(1)];
let target = base.clone();
let delta = diff_validators_iter(base.into_iter(), target.into_iter());
assert_eq!(delta.patches.len(), 0);
assert_eq!(delta.appended_validators.len(), 0);
}
#[test]
fn test_single_field_change_iter() {
let base = vec![MockValidator::new(0)];
let mut target = vec![MockValidator::new(0)];
target[0].effective_balance = 31_000_000_000;
let delta = diff_validators_iter(base.clone().into_iter(), target.clone().into_iter());
assert_eq!(delta.patches.len(), 1);
assert_eq!(delta.patches[0].field, ValidatorField::EffectiveBalance);
}
#[test]
fn test_appended_validators_iter() {
let base = vec![MockValidator::new(0)];
let target = vec![MockValidator::new(0), MockValidator::new(1)];
let delta = diff_validators_iter(base.into_iter(), target.into_iter());
assert_eq!(delta.patches.len(), 0);
assert_eq!(delta.appended_validators.len(), VALIDATOR_SSZ_SIZE);
}
#[test]
fn test_withdrawable_epoch_auto_reconstructed_non_slashed() {
let base = MockValidator::new(0);
let mut target = base.clone();
target.exit_epoch = 100;
target.withdrawable_epoch = 100 + MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
let delta = diff_validators_iter(
std::iter::once(base.clone()),
std::iter::once(target.clone()),
);
assert_eq!(delta.patches.len(), 1);
assert_eq!(delta.patches[0].field, ValidatorField::ExitEpoch);
let bytes = archive(&delta);
let archived = archived(&bytes);
let mut state = MockValidatorTarget {
validators: vec![base],
};
apply_validators_iter(&mut state, archived).expect("test setup: apply failed");
assert_eq!(state.validators[0], target);
}
#[test]
fn test_withdrawable_epoch_explicit_patch_slashed() {
let mut base = MockValidator::new(0);
base.slashed = true;
let mut target = base.clone();
target.exit_epoch = 100;
target.withdrawable_epoch = 150;
let delta = diff_validators_iter(
std::iter::once(base.clone()),
std::iter::once(target.clone()),
);
assert_eq!(delta.patches.len(), 2);
let fields: Vec<&ValidatorField> = delta.patches.iter().map(|p| &p.field).collect();
assert!(fields.contains(&&ValidatorField::ExitEpoch));
assert!(fields.contains(&&ValidatorField::WithdrawableEpochSlashed));
let bytes = archive(&delta);
let archived = archived(&bytes);
let mut state = MockValidatorTarget {
validators: vec![base],
};
apply_validators_iter(&mut state, archived).expect("test setup: apply failed");
assert_eq!(state.validators[0], target);
}
#[test]
fn test_byte_api_roundtrip() {
let base = [MockValidator::new(0), MockValidator::new(1)];
let target = [MockValidator::new(0), MockValidator::new(2)];
let base_bytes: Vec<u8> = base.iter().flat_map(|v| v.to_ssz_bytes()).collect();
let target_bytes: Vec<u8> = target.iter().flat_map(|v| v.to_ssz_bytes()).collect();
let delta = diff_validators(&base_bytes, &target_bytes);
let bytes = archive(&delta);
let archived = archived(&bytes);
let mut reconstructed = base_bytes;
apply_validators(&mut reconstructed, archived).expect("test setup: apply failed");
assert_eq!(reconstructed, target_bytes);
}
#[test]
fn test_apply_out_of_bounds_index() {
let base = vec![MockValidator::new(0)];
let mut target = vec![MockValidator::new(0)];
target[0].effective_balance = 100;
let delta = diff_validators_iter(base.into_iter(), target.into_iter());
let bytes = archive(&delta);
let archived = archived(&bytes);
let mut state = MockValidatorTarget { validators: vec![] };
let result = apply_validators_iter(&mut state, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(err_str.contains("out of bounds"));
}
#[test]
fn test_apply_invalid_patch_width() {
let delta = ValidatorsDiff {
patches: vec![ValidatorPatch {
index: 0,
field: ValidatorField::EffectiveBalance, value: vec![0; 4], }],
appended_validators: vec![],
};
let bytes = archive(&delta);
let archived = archived(&bytes);
let mut state = MockValidatorTarget {
validators: vec![MockValidator::new(0)],
};
let result = apply_validators_iter(&mut state, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(
err_str.contains("expected 8 bytes, got 4"),
"Error message should mention invalid width"
);
}
#[test]
fn test_apply_truncated_appended_validators() {
let delta = ValidatorsDiff {
patches: vec![],
appended_validators: vec![0u8; VALIDATOR_SSZ_SIZE - 1], };
let bytes = archive(&delta);
let archived = archived(&bytes);
let mut state = MockValidatorTarget { validators: vec![] };
let result = apply_validators_iter(&mut state, archived);
assert!(result.is_err());
let err_str = format!("{}", result.expect_err("test setup"));
assert!(
err_str.contains("does not contain complete SSZ records"),
"Error message should mention truncated appended data"
);
}
}