pub mod attestations;
pub mod balances;
pub mod eth1_data_votes;
pub mod historical_log;
pub mod inactivity_scores;
pub mod participation;
pub mod pending_queue;
pub mod randao_mixes;
pub mod recent_roots;
pub mod slashings;
pub mod sync_committee;
pub mod types;
pub mod validators;
pub mod error;
use error::Error;
use rkyv::{Archive, Deserialize, Serialize};
use crate::{
types::{
AttestationsDiff, BalancesDiff, Eth1DataVotesDiff, HistoricalLogDiff, InactivityDiff,
ParticipationDiff, QueueDiff, RandaoDiff, RootsDiff, SlashingsDiff, SyncCommitteeDiff,
ValidatorsDiff, HISTORICAL_ROOTS_SSZ_SIZE, HISTORICAL_SUMMARIES_SSZ_SIZE,
},
validators::{ValidatorMutTarget, ValidatorSnapshot},
};
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum ForkName {
Phase0 = 0,
Altair = 1,
Bellatrix = 2,
Capella = 3,
Deneb = 4,
Electra = 5,
Fulu = 6,
Gloas = 7,
Heze = 8,
}
pub trait DiffSource {
fn fork(&self) -> ForkName;
fn slot(&self) -> (u64, u64);
fn capella_fork_slot(&self) -> u64;
fn scalar_header(&self) -> Vec<u8>;
fn balances(
&self,
) -> (
impl ExactSizeIterator<Item = u64>,
impl ExactSizeIterator<Item = u64>,
);
fn validators(
&self,
) -> (
impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
);
fn block_roots(&self) -> &[[u8; 32]];
fn state_roots(&self) -> &[[u8; 32]];
fn randao_mixes(&self) -> &[[u8; 32]];
fn slashings(&self) -> (&[u64], &[u64]);
fn eth1_data_votes(&self) -> (&[u8], &[u8]);
fn historical_roots(&self) -> Option<&[u8]>;
fn previous_epoch_attestations(&self) -> Option<(&[u8], &[u8])>;
fn current_epoch_attestations(&self) -> Option<(&[u8], &[u8])>;
fn previous_participation(
&self,
) -> Option<(
impl ExactSizeIterator<Item = u8>,
impl ExactSizeIterator<Item = u8>,
)>;
fn current_participation(
&self,
) -> Option<(
impl ExactSizeIterator<Item = u8>,
impl ExactSizeIterator<Item = u8>,
)>;
fn inactivity_scores(&self) -> Option<(&[u64], &[u64])>;
fn current_sync_committee(&self) -> Option<(&[u8], &[u8])>;
fn next_sync_committee(&self) -> Option<(&[u8], &[u8])>;
fn historical_summaries(&self) -> Option<&[u8]>;
fn pending_deposits(&self) -> Option<(&[u8], &[u8])>;
fn pending_partial_withdrawals(&self) -> Option<(&[u8], &[u8])>;
fn pending_consolidations(&self) -> Option<(&[u8], &[u8])>;
}
pub trait DiffTarget {
fn get_fork(&self) -> ForkName;
fn scalar_header_mut(&mut self) -> &mut Vec<u8>;
fn balances_mut(&mut self) -> &mut impl ListMutTarget<u64>;
fn validators_mut(&mut self) -> &mut impl ValidatorMutTarget;
fn block_roots_mut(&mut self) -> &mut [[u8; 32]];
fn state_roots_mut(&mut self) -> &mut [[u8; 32]];
fn randao_mixes_mut(&mut self) -> &mut [[u8; 32]];
fn slashings_mut(&mut self) -> &mut [u64];
fn eth1_data_votes_mut(&mut self) -> &mut Vec<u8>;
fn historical_roots_mut(&mut self) -> Option<&mut Vec<u8>>;
fn previous_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>>;
fn current_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>>;
fn previous_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>>;
fn current_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>>;
fn inactivity_scores_mut(&mut self) -> Option<&mut Vec<u64>>;
fn current_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>>;
fn next_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>>;
fn historical_summaries_mut(&mut self) -> Option<&mut Vec<u8>>;
fn pending_deposits_mut(&mut self) -> Option<&mut Vec<u8>>;
fn pending_partial_withdrawals_mut(&mut self) -> Option<&mut Vec<u8>>;
fn pending_consolidations_mut(&mut self) -> Option<&mut Vec<u8>>;
}
#[derive(Archive, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct BeaconStateDelta {
pub fork: ForkName,
pub base_slot: u64,
pub scalar_header: Vec<u8>,
pub balances: BalancesDiff,
pub validators: ValidatorsDiff,
pub block_roots: RootsDiff,
pub state_roots: RootsDiff,
pub randao_mixes: RandaoDiff,
pub slashings: SlashingsDiff,
pub eth1_data_votes: Eth1DataVotesDiff,
pub historical_roots: Option<HistoricalLogDiff>,
pub previous_epoch_attestations: Option<AttestationsDiff>,
pub current_epoch_attestations: Option<AttestationsDiff>,
pub previous_participation: Option<ParticipationDiff>,
pub current_participation: Option<ParticipationDiff>,
pub inactivity_scores: Option<InactivityDiff>,
pub current_sync_committee: Option<SyncCommitteeDiff>,
pub next_sync_committee: Option<SyncCommitteeDiff>,
pub historical_summaries: Option<HistoricalLogDiff>,
pub pending_deposits: Option<QueueDiff>,
pub pending_partial_withdrawals: Option<QueueDiff>,
pub pending_consolidations: Option<QueueDiff>,
}
pub fn create<R: DiffSource>(state: &R) -> BeaconStateDelta {
let (base_slot, target_slot) = state.slot();
let delta = BeaconStateDelta {
fork: state.fork(),
base_slot,
scalar_header: state.scalar_header(),
balances: balances::diff_balances_iter(state.balances().0, state.balances().1),
validators: validators::diff_validators_iter(state.validators().0, state.validators().1),
block_roots: recent_roots::diff_roots(base_slot, target_slot, state.block_roots()),
state_roots: recent_roots::diff_roots(base_slot, target_slot, state.state_roots()),
randao_mixes: randao_mixes::diff_randao(base_slot, target_slot, state.randao_mixes()),
slashings: slashings::diff_slashings(
base_slot,
target_slot,
state.slashings().0,
state.slashings().1,
),
eth1_data_votes: eth1_data_votes::diff_eth1_votes(
state.eth1_data_votes().0,
state.eth1_data_votes().1,
),
historical_roots: state.historical_roots().map(|t| {
historical_log::diff_historical_log(
base_slot,
target_slot,
t,
HISTORICAL_ROOTS_SSZ_SIZE,
None,
)
}),
previous_epoch_attestations: state
.previous_epoch_attestations()
.map(|(b, t)| attestations::diff_attestations(b, t)),
current_epoch_attestations: state
.current_epoch_attestations()
.map(|(b, t)| attestations::diff_attestations(b, t)),
previous_participation: state
.previous_participation()
.map(|(b, t)| participation::diff_participation_iter(b, t)),
current_participation: state
.current_participation()
.map(|(b, t)| participation::diff_participation_iter(b, t)),
inactivity_scores: state
.inactivity_scores()
.map(|(b, t)| inactivity_scores::diff_inactivity(b, t)),
current_sync_committee: state
.current_sync_committee()
.map(|(b, t)| sync_committee::diff_sync_committee(b, t)),
next_sync_committee: state
.next_sync_committee()
.map(|(b, t)| sync_committee::diff_sync_committee(b, t)),
historical_summaries: state.historical_summaries().map(|t| {
historical_log::diff_historical_log(
base_slot,
target_slot,
t,
HISTORICAL_SUMMARIES_SSZ_SIZE,
Some(state.capella_fork_slot()),
)
}),
pending_deposits: state
.pending_deposits()
.map(|(b, t)| pending_queue::diff_queue(b, t, PENDING_DEPOSIT_SSZ_SIZE)),
pending_partial_withdrawals: state
.pending_partial_withdrawals()
.map(|(b, t)| pending_queue::diff_queue(b, t, PARTIAL_WITHDRAWAL_SSZ_SIZE)),
pending_consolidations: state
.pending_consolidations()
.map(|(b, t)| pending_queue::diff_queue(b, t, PENDING_CONSOLIDATION_SSZ_SIZE)),
};
debug_assert_eq!(
delta.previous_participation.is_some(),
delta.fork >= ForkName::Altair,
"DiffSource bug: previous_participation must exist iff fork >= Altair (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.current_participation.is_some(),
delta.fork >= ForkName::Altair,
"DiffSource bug: current_participation must exist iff fork >= Altair (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.inactivity_scores.is_some(),
delta.fork >= ForkName::Altair,
"DiffSource bug: inactivity_scores must exist iff fork >= Altair (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.current_sync_committee.is_some(),
delta.fork >= ForkName::Altair,
"DiffSource bug: current_sync_committee must exist iff fork >= Altair (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.next_sync_committee.is_some(),
delta.fork >= ForkName::Altair,
"DiffSource bug: next_sync_committee must exist iff fork >= Altair (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.historical_summaries.is_some(),
delta.fork >= ForkName::Capella,
"DiffSource bug: historical_summaries must exist iff fork >= Capella (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.historical_roots.is_some(),
delta.fork < ForkName::Capella,
"DiffSource bug: historical_roots must exist iff fork < Capella (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.pending_deposits.is_some(),
delta.fork >= ForkName::Electra,
"DiffSource bug: pending_deposits must exist iff fork >= Electra (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.pending_partial_withdrawals.is_some(),
delta.fork >= ForkName::Electra,
"DiffSource bug: pending_partial_withdrawals must exist iff fork >= Electra (got {:?})",
delta.fork
);
debug_assert_eq!(
delta.pending_consolidations.is_some(),
delta.fork >= ForkName::Electra,
"DiffSource bug: pending_consolidations must exist iff fork >= Electra (got {:?})",
delta.fork
);
delta
}
pub fn apply<M: DiffTarget>(mut state: M, delta: &ArchivedBeaconStateDelta) -> Result<M, Error> {
use rkyv::deserialize;
let delta_fork: ForkName = deserialize::<ForkName, rkyv::rancor::Error>(&delta.fork)
.map_err(|e| Error::MalformedDelta(format!("failed to deserialize fork: {e}")))?;
let state_fork = state.get_fork();
if state_fork != delta_fork {
return Err(Error::ForkMismatch {
state_fork,
delta_fork,
});
}
macro_rules! validate_removed_field {
($field:ident, $removed_in:expr) => {
if delta.$field.is_some() && delta_fork >= $removed_in {
return Err(Error::InvalidFieldForFork {
field: stringify!($field),
fork: delta_fork,
});
}
};
}
macro_rules! validate_field {
($field:ident, $fork:expr) => {
if delta.$field.is_some() && delta_fork < $fork {
return Err(Error::InvalidFieldForFork {
field: stringify!($field),
fork: delta_fork,
});
}
};
}
validate_field!(previous_participation, ForkName::Altair);
validate_field!(current_participation, ForkName::Altair);
validate_field!(inactivity_scores, ForkName::Altair);
validate_field!(current_sync_committee, ForkName::Altair);
validate_field!(next_sync_committee, ForkName::Altair);
validate_field!(historical_summaries, ForkName::Capella);
validate_field!(pending_deposits, ForkName::Electra);
validate_field!(pending_partial_withdrawals, ForkName::Electra);
validate_field!(pending_consolidations, ForkName::Electra);
validate_removed_field!(previous_epoch_attestations, ForkName::Altair);
validate_removed_field!(current_epoch_attestations, ForkName::Altair);
validate_removed_field!(historical_roots, ForkName::Capella);
let base_slot = delta.base_slot.to_native();
*state.scalar_header_mut() = delta.scalar_header.as_slice().to_vec();
balances::apply_balances_iter(state.balances_mut(), &delta.balances)?;
validators::apply_validators_iter(state.validators_mut(), &delta.validators)?;
recent_roots::apply_roots(base_slot, state.block_roots_mut(), &delta.block_roots)?;
recent_roots::apply_roots(base_slot, state.state_roots_mut(), &delta.state_roots)?;
randao_mixes::apply_randao(base_slot, state.randao_mixes_mut(), &delta.randao_mixes)?;
slashings::apply_slashings(state.slashings_mut(), &delta.slashings)?;
eth1_data_votes::apply_eth1_votes(state.eth1_data_votes_mut(), &delta.eth1_data_votes);
if let (Some(s), Some(d)) = (
state.historical_roots_mut(),
delta.historical_roots.as_ref(),
) {
historical_log::apply_historical_log(s, d);
}
if let (Some(s), Some(d)) = (
state.previous_epoch_attestations_mut(),
delta.previous_epoch_attestations.as_ref(),
) {
attestations::apply_attestations(s, d);
}
if let (Some(s), Some(d)) = (
state.current_epoch_attestations_mut(),
delta.current_epoch_attestations.as_ref(),
) {
attestations::apply_attestations(s, d);
}
if let (Some(s), Some(d)) = (
state.previous_participation_mut(),
delta.previous_participation.as_ref(),
) {
participation::apply_participation_iter(s, d)?;
}
if let (Some(s), Some(d)) = (
state.current_participation_mut(),
delta.current_participation.as_ref(),
) {
participation::apply_participation_iter(s, d)?;
}
if let (Some(s), Some(d)) = (
state.inactivity_scores_mut(),
delta.inactivity_scores.as_ref(),
) {
inactivity_scores::apply_inactivity(s, d)?;
}
if let (Some(s), Some(d)) = (
state.current_sync_committee_mut(),
delta.current_sync_committee.as_ref(),
) {
sync_committee::apply_sync_committee(s, d);
}
if let (Some(s), Some(d)) = (
state.next_sync_committee_mut(),
delta.next_sync_committee.as_ref(),
) {
sync_committee::apply_sync_committee(s, d);
}
if let (Some(s), Some(d)) = (
state.historical_summaries_mut(),
delta.historical_summaries.as_ref(),
) {
historical_log::apply_historical_log(s, d);
}
if let (Some(s), Some(d)) = (
state.pending_deposits_mut(),
delta.pending_deposits.as_ref(),
) {
pending_queue::apply_queue(s, d, PENDING_DEPOSIT_SSZ_SIZE)?;
}
if let (Some(s), Some(d)) = (
state.pending_partial_withdrawals_mut(),
delta.pending_partial_withdrawals.as_ref(),
) {
pending_queue::apply_queue(s, d, PARTIAL_WITHDRAWAL_SSZ_SIZE)?;
}
if let (Some(s), Some(d)) = (
state.pending_consolidations_mut(),
delta.pending_consolidations.as_ref(),
) {
pending_queue::apply_queue(s, d, PENDING_CONSOLIDATION_SSZ_SIZE)?;
}
Ok(state)
}
pub trait ListMutTarget<T: Copy> {
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn get_mut(&mut self, index: usize) -> Option<&mut T>;
fn push(&mut self, value: T);
}
impl ListMutTarget<u64> for Vec<u64> {
#[inline]
fn len(&self) -> usize {
self.len()
}
#[inline]
fn get_mut(&mut self, index: usize) -> Option<&mut u64> {
self.as_mut_slice().get_mut(index)
}
#[inline]
fn push(&mut self, value: u64) {
self.push(value);
}
}
impl ListMutTarget<u8> for Vec<u8> {
#[inline]
fn len(&self) -> usize {
self.len()
}
#[inline]
fn get_mut(&mut self, index: usize) -> Option<&mut u8> {
self.as_mut_slice().get_mut(index)
}
#[inline]
fn push(&mut self, value: u8) {
self.push(value);
}
}
const PENDING_DEPOSIT_SSZ_SIZE: usize = 192;
const PARTIAL_WITHDRAWAL_SSZ_SIZE: usize = 24;
const PENDING_CONSOLIDATION_SSZ_SIZE: usize = 16;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fork_name_ordering_is_correct() {
assert!(ForkName::Phase0 < ForkName::Altair);
assert!(ForkName::Altair < ForkName::Bellatrix);
assert!(ForkName::Bellatrix < ForkName::Capella);
assert!(ForkName::Capella < ForkName::Deneb);
assert!(ForkName::Deneb < ForkName::Electra);
assert_eq!(ForkName::Capella, ForkName::Capella);
}
#[test]
fn list_mut_target_vec_u64_works() {
let mut v = vec![100u64, 200, 300];
{
let target: &mut dyn ListMutTarget<u64> = &mut v;
assert_eq!(target.len(), 3);
assert!(!target.is_empty());
*target.get_mut(1).expect("index 1 exists") = 250;
assert!(target.get_mut(10).is_none());
target.push(400);
assert_eq!(target.len(), 4);
}
assert_eq!(v[1], 250);
assert_eq!(v[3], 400);
}
#[test]
fn list_mut_target_vec_u8_works() {
let mut v = vec![1u8, 2, 3];
{
let target: &mut dyn ListMutTarget<u8> = &mut v;
assert_eq!(target.len(), 3);
*target.get_mut(0).expect("index 0 exists") = 9;
target.push(4);
}
assert_eq!(v[0], 9);
assert_eq!(v[3], 4);
}
}
#[cfg(test)]
mod integration_tests {
use super::*;
use crate::types::{MIN_VALIDATOR_WITHDRAWABILITY_DELAY, VALIDATOR_SSZ_SIZE};
use crate::validators::{ValidatorMut, ValidatorMutTarget, ValidatorSnapshot};
const SLOTS_PER_EPOCH: u64 = 32;
const SLOTS_PER_HISTORICAL_ROOT: usize = 8192;
const EPOCHS_PER_HISTORICAL_ROOT: usize = 256;
const EPOCHS_PER_SLASHINGS_VECTOR: usize = 8192;
#[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: u64::MAX,
withdrawable_epoch: u64::MAX,
}
}
fn from_ssz_bytes(b: &[u8]) -> Self {
assert!(b.len() >= VALIDATOR_SSZ_SIZE, "ssz too short");
Self {
withdrawal_credentials: b[48..80].try_into().unwrap(),
effective_balance: u64::from_le_bytes(b[80..88].try_into().unwrap()),
slashed: b[88] != 0,
activation_eligibility_epoch: u64::from_le_bytes(b[89..97].try_into().unwrap()),
activation_epoch: u64::from_le_bytes(b[97..105].try_into().unwrap()),
exit_epoch: u64::from_le_bytes(b[105..113].try_into().unwrap()),
withdrawable_epoch: u64::from_le_bytes(b[113..121].try_into().unwrap()),
}
}
}
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> {
let mut b = vec![0u8; VALIDATOR_SSZ_SIZE];
b[48..80].copy_from_slice(&self.withdrawal_credentials);
b[80..88].copy_from_slice(&self.effective_balance.to_le_bytes());
b[88] = self.slashed as u8;
b[89..97].copy_from_slice(&self.activation_eligibility_epoch.to_le_bytes());
b[97..105].copy_from_slice(&self.activation_epoch.to_le_bytes());
b[105..113].copy_from_slice(&self.exit_epoch.to_le_bytes());
b[113..121].copy_from_slice(&self.withdrawable_epoch.to_le_bytes());
b
}
}
struct MockMutVal<'a>(&'a mut MockValidator);
impl ValidatorMut for MockMutVal<'_> {
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;
}
}
impl ValidatorMutTarget for Vec<MockValidator> {
type Validator<'a>
= MockMutVal<'a>
where
Self: 'a;
fn get_mut(&mut self, i: usize) -> Option<Self::Validator<'_>> {
self.as_mut_slice().get_mut(i).map(MockMutVal)
}
fn push_from_ssz(&mut self, b: &[u8]) {
self.push(MockValidator::from_ssz_bytes(b));
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct MockState {
fork: ForkName,
slot: u64,
capella_fork_slot: u64,
scalar_header: Vec<u8>,
balances: Vec<u64>,
validators: Vec<MockValidator>,
block_roots: Vec<[u8; 32]>,
state_roots: Vec<[u8; 32]>,
randao_mixes: Vec<[u8; 32]>,
slashings: Vec<u64>,
eth1_data_votes: Vec<u8>,
historical_roots: Option<Vec<u8>>,
previous_epoch_attestations: Option<Vec<u8>>,
current_epoch_attestations: Option<Vec<u8>>,
previous_participation: Option<Vec<u8>>,
current_participation: Option<Vec<u8>>,
inactivity_scores: Option<Vec<u64>>,
current_sync_committee: Option<Vec<u8>>,
next_sync_committee: Option<Vec<u8>>,
historical_summaries: Option<Vec<u8>>,
pending_deposits: Option<Vec<u8>>,
pending_partial_withdrawals: Option<Vec<u8>>,
pending_consolidations: Option<Vec<u8>>,
}
impl MockState {
fn at_fork(fork: ForkName, slot: u64, capella_fork_slot: u64) -> Self {
let mut state = Self {
fork: fork.clone(),
slot,
capella_fork_slot,
scalar_header: vec![],
balances: vec![],
validators: vec![],
block_roots: vec![[0; 32]; SLOTS_PER_HISTORICAL_ROOT],
state_roots: vec![[0; 32]; SLOTS_PER_HISTORICAL_ROOT],
randao_mixes: vec![[0; 32]; EPOCHS_PER_HISTORICAL_ROOT],
slashings: vec![0; EPOCHS_PER_SLASHINGS_VECTOR],
eth1_data_votes: vec![],
historical_roots: None,
previous_epoch_attestations: None,
current_epoch_attestations: None,
previous_participation: None,
current_participation: None,
inactivity_scores: None,
current_sync_committee: None,
next_sync_committee: None,
historical_summaries: None,
pending_deposits: None,
pending_partial_withdrawals: None,
pending_consolidations: None,
};
if fork == ForkName::Phase0 {
state.previous_epoch_attestations = Some(vec![]);
state.current_epoch_attestations = Some(vec![]);
}
if fork < ForkName::Capella {
state.historical_roots = Some(vec![]);
}
if fork >= ForkName::Altair {
state.previous_participation = Some(vec![]);
state.current_participation = Some(vec![]);
state.inactivity_scores = Some(vec![]);
state.current_sync_committee = Some(vec![0; 48 * 512]);
state.next_sync_committee = Some(vec![0; 48 * 512]);
}
if fork >= ForkName::Capella {
state.historical_summaries = Some(vec![]);
}
if fork >= ForkName::Electra {
state.pending_deposits = Some(vec![]);
state.pending_partial_withdrawals = Some(vec![]);
state.pending_consolidations = Some(vec![]);
}
state
}
}
struct MockSource<'a> {
base: &'a MockState,
target: &'a MockState,
}
fn pair_ref<'a, T>(base: &'a Option<T>, target: &'a Option<T>) -> Option<(&'a T, &'a T)> {
match (base, target) {
(Some(base), Some(target)) => Some((base, target)),
_ => None,
}
}
impl DiffSource for MockSource<'_> {
fn fork(&self) -> ForkName {
self.target.fork.clone()
}
fn slot(&self) -> (u64, u64) {
(self.base.slot, self.target.slot)
}
fn capella_fork_slot(&self) -> u64 {
self.target.capella_fork_slot
}
fn scalar_header(&self) -> Vec<u8> {
self.target.scalar_header.clone()
}
fn balances(
&self,
) -> (
impl ExactSizeIterator<Item = u64>,
impl ExactSizeIterator<Item = u64>,
) {
(
self.base.balances.clone().into_iter(),
self.target.balances.clone().into_iter(),
)
}
fn validators(
&self,
) -> (
impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
impl ExactSizeIterator<Item = impl ValidatorSnapshot>,
) {
(
self.base.validators.clone().into_iter(),
self.target.validators.clone().into_iter(),
)
}
fn block_roots(&self) -> &[[u8; 32]] {
&self.target.block_roots
}
fn state_roots(&self) -> &[[u8; 32]] {
&self.target.state_roots
}
fn randao_mixes(&self) -> &[[u8; 32]] {
&self.target.randao_mixes
}
fn slashings(&self) -> (&[u64], &[u64]) {
(&self.base.slashings, &self.target.slashings)
}
fn eth1_data_votes(&self) -> (&[u8], &[u8]) {
(&self.base.eth1_data_votes, &self.target.eth1_data_votes)
}
fn historical_roots(&self) -> Option<&[u8]> {
self.target.historical_roots.as_deref()
}
fn previous_epoch_attestations(&self) -> Option<(&[u8], &[u8])> {
pair_ref(
&self.base.previous_epoch_attestations,
&self.target.previous_epoch_attestations,
)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn current_epoch_attestations(&self) -> Option<(&[u8], &[u8])> {
pair_ref(
&self.base.current_epoch_attestations,
&self.target.current_epoch_attestations,
)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn previous_participation(
&self,
) -> Option<(
impl ExactSizeIterator<Item = u8>,
impl ExactSizeIterator<Item = u8>,
)> {
pair_ref(
&self.base.previous_participation,
&self.target.previous_participation,
)
.map(|(base, target)| (base.clone().into_iter(), target.clone().into_iter()))
}
fn current_participation(
&self,
) -> Option<(
impl ExactSizeIterator<Item = u8>,
impl ExactSizeIterator<Item = u8>,
)> {
pair_ref(
&self.base.current_participation,
&self.target.current_participation,
)
.map(|(base, target)| (base.clone().into_iter(), target.clone().into_iter()))
}
fn inactivity_scores(&self) -> Option<(&[u64], &[u64])> {
pair_ref(&self.base.inactivity_scores, &self.target.inactivity_scores)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn current_sync_committee(&self) -> Option<(&[u8], &[u8])> {
pair_ref(
&self.base.current_sync_committee,
&self.target.current_sync_committee,
)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn next_sync_committee(&self) -> Option<(&[u8], &[u8])> {
pair_ref(
&self.base.next_sync_committee,
&self.target.next_sync_committee,
)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn historical_summaries(&self) -> Option<&[u8]> {
self.target.historical_summaries.as_deref()
}
fn pending_deposits(&self) -> Option<(&[u8], &[u8])> {
pair_ref(&self.base.pending_deposits, &self.target.pending_deposits)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn pending_partial_withdrawals(&self) -> Option<(&[u8], &[u8])> {
pair_ref(
&self.base.pending_partial_withdrawals,
&self.target.pending_partial_withdrawals,
)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
fn pending_consolidations(&self) -> Option<(&[u8], &[u8])> {
pair_ref(
&self.base.pending_consolidations,
&self.target.pending_consolidations,
)
.map(|(base, target)| (base.as_slice(), target.as_slice()))
}
}
impl DiffTarget for MockState {
fn get_fork(&self) -> ForkName {
self.fork.clone()
}
fn scalar_header_mut(&mut self) -> &mut Vec<u8> {
&mut self.scalar_header
}
fn balances_mut(&mut self) -> &mut impl ListMutTarget<u64> {
&mut self.balances
}
fn validators_mut(&mut self) -> &mut impl ValidatorMutTarget {
&mut self.validators
}
fn block_roots_mut(&mut self) -> &mut [[u8; 32]] {
self.block_roots.as_mut_slice()
}
fn state_roots_mut(&mut self) -> &mut [[u8; 32]] {
self.state_roots.as_mut_slice()
}
fn randao_mixes_mut(&mut self) -> &mut [[u8; 32]] {
self.randao_mixes.as_mut_slice()
}
fn slashings_mut(&mut self) -> &mut [u64] {
self.slashings.as_mut_slice()
}
fn eth1_data_votes_mut(&mut self) -> &mut Vec<u8> {
&mut self.eth1_data_votes
}
fn historical_roots_mut(&mut self) -> Option<&mut Vec<u8>> {
self.historical_roots.as_mut()
}
fn previous_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>> {
self.previous_epoch_attestations.as_mut()
}
fn current_epoch_attestations_mut(&mut self) -> Option<&mut Vec<u8>> {
self.current_epoch_attestations.as_mut()
}
fn previous_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>> {
self.previous_participation.as_mut()
}
fn current_participation_mut(&mut self) -> Option<&mut impl ListMutTarget<u8>> {
self.current_participation.as_mut()
}
fn inactivity_scores_mut(&mut self) -> Option<&mut Vec<u64>> {
self.inactivity_scores.as_mut()
}
fn current_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>> {
self.current_sync_committee.as_mut()
}
fn next_sync_committee_mut(&mut self) -> Option<&mut Vec<u8>> {
self.next_sync_committee.as_mut()
}
fn historical_summaries_mut(&mut self) -> Option<&mut Vec<u8>> {
self.historical_summaries.as_mut()
}
fn pending_deposits_mut(&mut self) -> Option<&mut Vec<u8>> {
self.pending_deposits.as_mut()
}
fn pending_partial_withdrawals_mut(&mut self) -> Option<&mut Vec<u8>> {
self.pending_partial_withdrawals.as_mut()
}
fn pending_consolidations_mut(&mut self) -> Option<&mut Vec<u8>> {
self.pending_consolidations.as_mut()
}
}
fn archive_delta(delta: &BeaconStateDelta) -> Vec<u8> {
rkyv::to_bytes::<rkyv::rancor::Error>(delta)
.expect("serialize delta")
.to_vec()
}
fn access_archived(bytes: &[u8]) -> &ArchivedBeaconStateDelta {
rkyv::access::<ArchivedBeaconStateDelta, rkyv::rancor::Error>(bytes)
.expect("access archived delta")
}
fn roundtrip(base: MockState, target: MockState) {
assert_eq!(
base.fork, target.fork,
"roundtrip requires base and target to use the same fork"
);
let source = MockSource {
base: &base,
target: &target,
};
let delta = create(&source);
let bytes = archive_delta(&delta);
let archived = access_archived(&bytes);
let mut reconstructed = apply(base.clone(), archived).expect("apply");
reconstructed.slot = target.slot;
assert_eq!(reconstructed.fork, target.fork, "fork mismatch");
assert_eq!(
reconstructed.scalar_header, target.scalar_header,
"scalar_header mismatch"
);
assert_eq!(reconstructed.balances, target.balances, "balances mismatch");
assert_eq!(
reconstructed.validators, target.validators,
"validators mismatch"
);
assert_eq!(
reconstructed.block_roots, target.block_roots,
"block_roots mismatch"
);
assert_eq!(
reconstructed.state_roots, target.state_roots,
"state_roots mismatch"
);
assert_eq!(
reconstructed.randao_mixes, target.randao_mixes,
"randao_mixes mismatch"
);
assert_eq!(
reconstructed.slashings, target.slashings,
"slashings mismatch"
);
assert_eq!(
reconstructed.eth1_data_votes, target.eth1_data_votes,
"eth1_data_votes mismatch"
);
assert_eq!(
reconstructed.historical_roots, target.historical_roots,
"historical_roots mismatch"
);
assert_eq!(
reconstructed.previous_epoch_attestations, target.previous_epoch_attestations,
"previous_epoch_attestations mismatch"
);
assert_eq!(
reconstructed.current_epoch_attestations, target.current_epoch_attestations,
"current_epoch_attestations mismatch"
);
assert_eq!(
reconstructed.previous_participation, target.previous_participation,
"previous_participation mismatch"
);
assert_eq!(
reconstructed.current_participation, target.current_participation,
"current_participation mismatch"
);
assert_eq!(
reconstructed.inactivity_scores, target.inactivity_scores,
"inactivity_scores mismatch"
);
assert_eq!(
reconstructed.current_sync_committee, target.current_sync_committee,
"current_sync_committee mismatch"
);
assert_eq!(
reconstructed.next_sync_committee, target.next_sync_committee,
"next_sync_committee mismatch"
);
assert_eq!(
reconstructed.historical_summaries, target.historical_summaries,
"historical_summaries mismatch"
);
assert_eq!(
reconstructed.pending_deposits, target.pending_deposits,
"pending_deposits mismatch"
);
assert_eq!(
reconstructed.pending_partial_withdrawals, target.pending_partial_withdrawals,
"pending_partial_withdrawals mismatch"
);
assert_eq!(
reconstructed.pending_consolidations, target.pending_consolidations,
"pending_consolidations mismatch"
);
}
#[test]
fn phase0_empty_state_no_changes() {
let base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
let target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
roundtrip(base, target);
}
#[test]
fn phase0_balances_validators_roots_slashings_votes() {
let mut base = MockState::at_fork(ForkName::Phase0, 96, 32_000);
let mut target = MockState::at_fork(ForkName::Phase0, 128, 32_000);
base.validators = vec![MockValidator::new(1), MockValidator::new(2)];
base.balances = vec![32_000_000_000, 32_000_000_000];
target.validators = base.validators.clone();
target.balances = base.balances.clone();
target.balances[1] = 31_000_000_000;
target.validators[0].effective_balance = 31_000_000_000;
target.validators.push(MockValidator::new(3));
target.balances.push(32_000_000_000);
for slot in 96u64..128 {
let index = (slot as usize) % SLOTS_PER_HISTORICAL_ROOT;
target.block_roots[index] = [slot as u8; 32];
target.state_roots[index] = [(slot + 1) as u8; 32];
}
let slashing_index = (target.slot / SLOTS_PER_EPOCH) as usize % EPOCHS_PER_SLASHINGS_VECTOR;
target.slashings[slashing_index] = 1_000_000_000;
target.eth1_data_votes = vec![0xAA, 0xBB, 0xCC];
base.scalar_header = vec![0x11; 16];
target.scalar_header = vec![0x22; 16];
roundtrip(base, target);
}
#[test]
fn capella_with_altair_fields_and_slashed_validator() {
let mut base = MockState::at_fork(ForkName::Capella, 32, 32);
let mut target = MockState::at_fork(ForkName::Capella, 8224, 32);
base.validators.clone_from(&vec![
MockValidator::new(1),
MockValidator::new(2),
MockValidator::new(3),
]);
target.validators.clone_from(&base.validators);
base.balances
.clone_from(&vec![32_000_000_000, 32_000_000_000, 32_000_000_000]);
target.balances.clone_from(&base.balances);
target.validators[1].slashed = true;
target.validators[1].exit_epoch = 200;
target.validators[1].withdrawable_epoch = 200 + MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
target.balances[2] -= 1_000_000_000;
for i in 0..5 {
target.block_roots[100 + i] = [i as u8; 32];
target.state_roots[100 + i] = [(i + 10) as u8; 32];
}
target.previous_participation = Some(vec![1, 3, 7]);
target.current_participation = Some(vec![2, 4, 8]);
target.inactivity_scores = Some(vec![0, 1, 2]);
target.current_sync_committee = Some(vec![0xFF; 48 * 512]);
target.next_sync_committee = Some(vec![0xEE; 48 * 512]);
target.historical_summaries = Some(vec![0xAB; 64]);
target.scalar_header = vec![0x44];
roundtrip(base, target);
}
#[test]
fn electra_with_pending_queues() {
let mut base = MockState::at_fork(ForkName::Electra, 100, 32);
let mut target = MockState::at_fork(ForkName::Electra, 105, 32);
base.validators = vec![MockValidator::new(1)];
base.balances = vec![32_000_000_000];
target.validators = base.validators.clone();
target.balances = base.balances.clone();
target.pending_deposits = Some(vec![0xAB; PENDING_DEPOSIT_SSZ_SIZE]);
target.pending_partial_withdrawals = Some(vec![0xCD; PARTIAL_WITHDRAWAL_SSZ_SIZE]);
target.pending_consolidations = Some(vec![0xEF; PENDING_CONSOLIDATION_SSZ_SIZE]);
base.scalar_header = vec![0x55; 16];
target.scalar_header = vec![0x66; 16];
roundtrip(base, target);
}
#[test]
fn fork_mismatch_rejected() {
let base = MockState::at_fork(ForkName::Capella, 100, 32);
let target = MockState::at_fork(ForkName::Capella, 105, 32);
let source = MockSource {
base: &base,
target: &target,
};
let delta = create(&source);
assert_eq!(delta.fork, ForkName::Capella);
let bytes = archive_delta(&delta);
let archived = access_archived(&bytes);
let state = MockState::at_fork(ForkName::Phase0, 100, 32_000);
let result = apply(state, archived);
assert!(
matches!(
result,
Err(Error::ForkMismatch {
state_fork: ForkName::Phase0,
delta_fork: ForkName::Capella,
})
),
"expected ForkMismatch, got {result:?}"
);
}
#[test]
fn historical_roots_on_capella_rejected_by_apply() {
let base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
let target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
let source = MockSource {
base: &base,
target: &target,
};
let phase0_delta = create(&source);
assert!(phase0_delta.historical_roots.is_some());
let capella_base = MockState::at_fork(ForkName::Capella, 100, 32);
let capella_target = MockState::at_fork(ForkName::Capella, 105, 32);
let capella_source = MockSource {
base: &capella_base,
target: &capella_target,
};
let mut delta = create(&capella_source);
assert!(delta.historical_roots.is_none());
delta.historical_roots = phase0_delta.historical_roots;
assert!(delta.historical_roots.is_some());
let bytes = archive_delta(&delta);
let archived = access_archived(&bytes);
let state = MockState::at_fork(ForkName::Capella, 100, 32);
let result = apply(state, archived);
assert!(
matches!(
result,
Err(Error::InvalidFieldForFork {
field: "historical_roots",
fork: ForkName::Capella,
})
),
"expected InvalidFieldForFork, got {result:?}"
);
}
#[test]
fn altair_field_on_phase0_rejected_by_apply() {
let altair_base = MockState::at_fork(ForkName::Altair, 100, 32_000);
let mut altair_target = MockState::at_fork(ForkName::Altair, 105, 32_000);
altair_target.previous_participation = Some(vec![1, 2, 3]);
let altair_source = MockSource {
base: &altair_base,
target: &altair_target,
};
let altair_delta = create(&altair_source);
assert!(altair_delta.previous_participation.is_some());
let phase0_base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
let phase0_target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
let phase0_source = MockSource {
base: &phase0_base,
target: &phase0_target,
};
let mut delta = create(&phase0_source);
assert!(delta.previous_participation.is_none());
delta.previous_participation = altair_delta.previous_participation;
assert!(delta.previous_participation.is_some());
let bytes = archive_delta(&delta);
let archived = access_archived(&bytes);
let state = MockState::at_fork(ForkName::Phase0, 100, 32_000);
let result = apply(state, archived);
assert!(
matches!(
result,
Err(Error::InvalidFieldForFork {
field: "previous_participation",
fork: ForkName::Phase0,
})
),
"expected InvalidFieldForFork, got {result:?}"
);
}
#[test]
fn phase0_attestations_on_altair_rejected_by_apply() {
let phase0_base = MockState::at_fork(ForkName::Phase0, 100, 32_000);
let mut phase0_target = MockState::at_fork(ForkName::Phase0, 105, 32_000);
phase0_target.previous_epoch_attestations = Some(vec![0xAA]);
phase0_target.current_epoch_attestations = Some(vec![0xBB]);
let phase0_source = MockSource {
base: &phase0_base,
target: &phase0_target,
};
let phase0_delta = create(&phase0_source);
assert!(phase0_delta.previous_epoch_attestations.is_some());
assert!(phase0_delta.current_epoch_attestations.is_some());
let altair_base = MockState::at_fork(ForkName::Altair, 100, 32_000);
let altair_target = MockState::at_fork(ForkName::Altair, 105, 32_000);
let altair_source = MockSource {
base: &altair_base,
target: &altair_target,
};
let mut delta = create(&altair_source);
assert!(delta.previous_epoch_attestations.is_none());
assert!(delta.current_epoch_attestations.is_none());
delta.previous_epoch_attestations = phase0_delta.previous_epoch_attestations;
delta.current_epoch_attestations = phase0_delta.current_epoch_attestations;
let bytes = archive_delta(&delta);
let archived = access_archived(&bytes);
let state = MockState::at_fork(ForkName::Altair, 100, 32_000);
let result = apply(state, archived);
assert!(
matches!(
result,
Err(Error::InvalidFieldForFork {
field: "previous_epoch_attestations",
fork: ForkName::Altair,
})
),
"expected InvalidFieldForFork, got {result:?}"
);
}
}