1use crate::{
132 error::Error,
133 types::{
134 ArchivedValidatorField, ArchivedValidatorsDiff, ValidatorField, ValidatorPatch,
135 ValidatorsDiff, MIN_VALIDATOR_WITHDRAWABILITY_DELAY, VALIDATOR_SSZ_SIZE,
136 },
137};
138
139pub trait ValidatorSnapshot {
149 fn withdrawal_credentials(&self) -> &[u8; 32];
150 fn effective_balance(&self) -> u64;
151 fn is_slashed(&self) -> bool;
152 fn activation_eligibility_epoch(&self) -> u64;
153 fn activation_epoch(&self) -> u64;
154 fn exit_epoch(&self) -> u64;
155 fn withdrawable_epoch(&self) -> u64;
156
157 fn to_ssz_bytes(&self) -> Vec<u8>;
160}
161
162pub trait ValidatorMut {
171 fn is_slashed(&self) -> bool;
174
175 fn set_withdrawal_credentials(&mut self, value: &[u8; 32]);
176 fn set_effective_balance(&mut self, value: u64);
177 fn set_slashed(&mut self, value: bool);
178 fn set_activation_eligibility_epoch(&mut self, value: u64);
179 fn set_activation_epoch(&mut self, value: u64);
180 fn set_exit_epoch(&mut self, value: u64);
181 fn set_withdrawable_epoch(&mut self, value: u64);
182}
183
184pub trait ValidatorMutTarget {
197 type Validator<'a>: ValidatorMut
198 where
199 Self: 'a;
200
201 fn get_mut(&mut self, index: usize) -> Option<Self::Validator<'_>>;
203
204 fn push_from_ssz(&mut self, ssz_bytes: &[u8]);
206}
207
208pub fn diff_validators(base_bytes: &[u8], target_bytes: &[u8]) -> ValidatorsDiff {
235 diff_validators_impl(
236 base_bytes
237 .chunks_exact(VALIDATOR_SSZ_SIZE)
238 .map(ByteValidator::new),
239 target_bytes
240 .chunks_exact(VALIDATOR_SSZ_SIZE)
241 .map(ByteValidator::new),
242 )
243}
244
245pub fn diff_validators_iter<I1, I2, V1, V2>(base: I1, target: I2) -> ValidatorsDiff
265where
266 I1: ExactSizeIterator<Item = V1>,
267 I2: ExactSizeIterator<Item = V2>,
268 V1: ValidatorSnapshot,
269 V2: ValidatorSnapshot,
270{
271 diff_validators_impl(base, target)
272}
273
274fn diff_validators_impl<I1, I2, V1, V2>(mut base: I1, mut target: I2) -> ValidatorsDiff
276where
277 I1: ExactSizeIterator<Item = V1>,
278 I2: ExactSizeIterator<Item = V2>,
279 V1: ValidatorSnapshot,
280 V2: ValidatorSnapshot,
281{
282 let mut patches = Vec::with_capacity(512);
283 let mut appended_validators = Vec::new();
284
285 for (i, (b, t)) in base.by_ref().zip(target.by_ref()).enumerate() {
286 let wc = t.withdrawal_credentials();
287 let eb = t.effective_balance();
288 let slashed = t.is_slashed();
289 let aee = t.activation_eligibility_epoch();
290 let ae = t.activation_epoch();
291 let ee = t.exit_epoch();
292
293 if b.withdrawal_credentials() == wc
294 && b.effective_balance() == eb
295 && b.is_slashed() == slashed
296 && b.activation_eligibility_epoch() == aee
297 && b.activation_epoch() == ae
298 && b.exit_epoch() == ee
299 && (!slashed || b.withdrawable_epoch() == t.withdrawable_epoch())
300 {
301 continue;
302 }
303
304 let index = u32::try_from(i).expect("validator index exceeds u32 range");
305
306 if b.withdrawal_credentials() != wc {
307 patches.push(ValidatorPatch {
308 index,
309 field: ValidatorField::WithdrawalCredentials,
310 value: wc.to_vec(),
311 });
312 }
313 if b.effective_balance() != eb {
314 patches.push(ValidatorPatch {
315 index,
316 field: ValidatorField::EffectiveBalance,
317 value: eb.to_le_bytes().to_vec(),
318 });
319 }
320 if b.is_slashed() != slashed {
321 patches.push(ValidatorPatch {
322 index,
323 field: ValidatorField::Slashed,
324 value: vec![slashed as u8],
325 });
326 }
327 if b.activation_eligibility_epoch() != aee {
328 patches.push(ValidatorPatch {
329 index,
330 field: ValidatorField::ActivationEligibilityEpoch,
331 value: aee.to_le_bytes().to_vec(),
332 });
333 }
334 if b.activation_epoch() != ae {
335 patches.push(ValidatorPatch {
336 index,
337 field: ValidatorField::ActivationEpoch,
338 value: ae.to_le_bytes().to_vec(),
339 });
340 }
341 if b.exit_epoch() != ee {
342 patches.push(ValidatorPatch {
343 index,
344 field: ValidatorField::ExitEpoch,
345 value: ee.to_le_bytes().to_vec(),
346 });
347 }
348 if slashed && b.withdrawable_epoch() != t.withdrawable_epoch() {
349 patches.push(ValidatorPatch {
350 index,
351 field: ValidatorField::WithdrawableEpochSlashed,
352 value: t.withdrawable_epoch().to_le_bytes().to_vec(),
353 });
354 }
355 }
356
357 for t_val in target {
358 appended_validators.extend(t_val.to_ssz_bytes());
359 }
360
361 ValidatorsDiff {
362 patches,
363 appended_validators,
364 }
365}
366
367pub fn apply_validators(base: &mut Vec<u8>, delta: &ArchivedValidatorsDiff) -> Result<(), Error> {
396 apply_validators_iter(&mut ByteValidatorTarget(base), delta)
397}
398
399pub fn apply_validators_iter<T: ValidatorMutTarget>(
439 target: &mut T,
440 delta: &ArchivedValidatorsDiff,
441) -> Result<(), Error> {
442 for patch in delta.patches.iter() {
443 let idx = patch.index.to_native() as usize;
444 let val_bytes = patch.value.as_slice();
445
446 let mut validator = target.get_mut(idx).ok_or_else(|| {
447 Error::InvalidDelta(format!("validator patch index {idx} is out of bounds"))
448 })?;
449
450 match &patch.field {
451 ArchivedValidatorField::WithdrawalCredentials => {
452 let bytes: [u8; 32] = val_bytes.try_into().map_err(|_| {
453 Error::MalformedDelta(format!(
454 "withdrawal credentials patch has invalid width: \
455 expected 32 bytes, got {}",
456 val_bytes.len()
457 ))
458 })?;
459
460 validator.set_withdrawal_credentials(&bytes);
461 }
462 ArchivedValidatorField::EffectiveBalance => {
463 let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
464 Error::MalformedDelta(format!(
465 "effective balance patch has invalid width: \
466 expected 8 bytes, got {}",
467 val_bytes.len()
468 ))
469 })?;
470
471 let eb = u64::from_le_bytes(bytes);
472 validator.set_effective_balance(eb);
473 }
474 ArchivedValidatorField::Slashed => {
475 let [value] = <[u8; 1]>::try_from(val_bytes).map_err(|_| {
476 Error::MalformedDelta(format!(
477 "slashed patch has invalid width: expected 1 byte, got {}",
478 val_bytes.len()
479 ))
480 })?;
481
482 validator.set_slashed(value != 0);
483 }
484 ArchivedValidatorField::ActivationEligibilityEpoch => {
485 let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
486 Error::MalformedDelta(format!(
487 "activation eligibility epoch patch has invalid width: \
488 expected 8 bytes, got {}",
489 val_bytes.len()
490 ))
491 })?;
492
493 let epoch = u64::from_le_bytes(bytes);
494 validator.set_activation_eligibility_epoch(epoch);
495 }
496 ArchivedValidatorField::ActivationEpoch => {
497 let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
498 Error::MalformedDelta(format!(
499 "activation epoch patch has invalid width: \
500 expected 8 bytes, got {}",
501 val_bytes.len()
502 ))
503 })?;
504
505 let epoch = u64::from_le_bytes(bytes);
506 validator.set_activation_epoch(epoch);
507 }
508 ArchivedValidatorField::ExitEpoch => {
509 let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
510 Error::MalformedDelta(format!(
511 "exit epoch patch has invalid width: \
512 expected 8 bytes, got {}",
513 val_bytes.len()
514 ))
515 })?;
516
517 let ee = u64::from_le_bytes(bytes);
518 validator.set_exit_epoch(ee);
519
520 if !validator.is_slashed() {
522 let we = ee.saturating_add(MIN_VALIDATOR_WITHDRAWABILITY_DELAY);
523 validator.set_withdrawable_epoch(we);
524 }
525 }
526 ArchivedValidatorField::WithdrawableEpochSlashed => {
527 let bytes: [u8; 8] = val_bytes.try_into().map_err(|_| {
528 Error::MalformedDelta(format!(
529 "withdrawable epoch patch has invalid width: \
530 expected 8 bytes, got {}",
531 val_bytes.len()
532 ))
533 })?;
534
535 let we = u64::from_le_bytes(bytes);
536 validator.set_withdrawable_epoch(we);
537 }
538 }
539 }
540
541 if delta.appended_validators.len() % VALIDATOR_SSZ_SIZE != 0 {
542 return Err(Error::MalformedDelta(
543 "appended validator data does not contain complete SSZ records".into(),
544 ));
545 }
546
547 for chunk in delta
548 .appended_validators
549 .as_slice()
550 .chunks_exact(VALIDATOR_SSZ_SIZE)
551 {
552 target.push_from_ssz(chunk);
553 }
554
555 Ok(())
556}
557
558struct ByteValidator<'a>(&'a [u8]);
563
564impl<'a> ByteValidator<'a> {
565 fn new(bytes: &'a [u8]) -> Self {
566 debug_assert_eq!(
567 bytes.len(),
568 VALIDATOR_SSZ_SIZE,
569 "ByteValidator must contain exactly one complete SSZ validator record",
570 );
571
572 Self(bytes)
573 }
574
575 #[inline]
576 fn bytes<const N: usize>(&self, start: usize) -> [u8; N] {
577 self.0
578 .get(start..start + N)
579 .and_then(|bytes| bytes.try_into().ok())
580 .expect("ByteValidator contains a complete SSZ validator record")
581 }
582}
583
584impl<'a> ValidatorSnapshot for ByteValidator<'a> {
585 #[inline]
586 fn withdrawal_credentials(&self) -> &[u8; 32] {
587 self.0
588 .get(48..80)
589 .and_then(|bytes| bytes.try_into().ok())
590 .expect("ByteValidator contains a complete SSZ validator record")
591 }
592
593 #[inline]
594 fn effective_balance(&self) -> u64 {
595 u64::from_le_bytes(self.bytes::<8>(80))
596 }
597
598 #[inline]
599 fn is_slashed(&self) -> bool {
600 self.bytes::<1>(88)[0] != 0
601 }
602
603 #[inline]
604 fn activation_eligibility_epoch(&self) -> u64 {
605 u64::from_le_bytes(self.bytes::<8>(89))
606 }
607
608 #[inline]
609 fn activation_epoch(&self) -> u64 {
610 u64::from_le_bytes(self.bytes::<8>(97))
611 }
612
613 #[inline]
614 fn exit_epoch(&self) -> u64 {
615 u64::from_le_bytes(self.bytes::<8>(105))
616 }
617
618 #[inline]
619 fn withdrawable_epoch(&self) -> u64 {
620 u64::from_le_bytes(self.bytes::<8>(113))
621 }
622
623 #[inline]
624 fn to_ssz_bytes(&self) -> Vec<u8> {
625 self.0.to_vec()
626 }
627}
628
629struct ByteValidatorMut<'a>(&'a mut [u8]);
631
632impl<'a> ValidatorMut for ByteValidatorMut<'a> {
633 #[inline]
634 fn is_slashed(&self) -> bool {
635 *self
636 .0
637 .get(88)
638 .expect("ByteValidatorMut contains a complete SSZ validator record")
639 != 0
640 }
641
642 #[inline]
643 fn set_withdrawal_credentials(&mut self, v: &[u8; 32]) {
644 self.0
645 .get_mut(48..80)
646 .expect("ByteValidatorMut contains a complete SSZ validator record")
647 .copy_from_slice(v);
648 }
649
650 #[inline]
651 fn set_effective_balance(&mut self, v: u64) {
652 self.0
653 .get_mut(80..88)
654 .expect("ByteValidatorMut contains a complete SSZ validator record")
655 .copy_from_slice(&v.to_le_bytes());
656 }
657
658 #[inline]
659 fn set_slashed(&mut self, v: bool) {
660 *self
661 .0
662 .get_mut(88)
663 .expect("ByteValidatorMut contains a complete SSZ validator record") = v as u8;
664 }
665
666 #[inline]
667 fn set_activation_eligibility_epoch(&mut self, v: u64) {
668 self.0
669 .get_mut(89..97)
670 .expect("ByteValidatorMut contains a complete SSZ validator record")
671 .copy_from_slice(&v.to_le_bytes());
672 }
673
674 #[inline]
675 fn set_activation_epoch(&mut self, v: u64) {
676 self.0
677 .get_mut(97..105)
678 .expect("ByteValidatorMut contains a complete SSZ validator record")
679 .copy_from_slice(&v.to_le_bytes());
680 }
681
682 #[inline]
683 fn set_exit_epoch(&mut self, v: u64) {
684 self.0
685 .get_mut(105..113)
686 .expect("ByteValidatorMut contains a complete SSZ validator record")
687 .copy_from_slice(&v.to_le_bytes());
688 }
689
690 #[inline]
691 fn set_withdrawable_epoch(&mut self, v: u64) {
692 self.0
693 .get_mut(113..121)
694 .expect("ByteValidatorMut contains a complete SSZ validator record")
695 .copy_from_slice(&v.to_le_bytes());
696 }
697}
698
699struct ByteValidatorTarget<'a>(&'a mut Vec<u8>);
701
702impl<'a> ValidatorMutTarget for ByteValidatorTarget<'a> {
703 type Validator<'b>
704 = ByteValidatorMut<'b>
705 where
706 Self: 'b;
707
708 fn get_mut(&mut self, index: usize) -> Option<Self::Validator<'_>> {
709 let start = index.checked_mul(VALIDATOR_SSZ_SIZE)?;
710 let end = start.checked_add(VALIDATOR_SSZ_SIZE)?;
711 self.0.get_mut(start..end).map(ByteValidatorMut)
712 }
713
714 fn push_from_ssz(&mut self, ssz_bytes: &[u8]) {
715 self.0.extend_from_slice(ssz_bytes);
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722 use crate::types::{ArchivedValidatorsDiff, ValidatorField};
723
724 fn archive(diff: &ValidatorsDiff) -> rkyv::util::AlignedVec {
725 rkyv::to_bytes::<rkyv::rancor::Error>(diff).expect("test setup: failed to serialize delta")
726 }
727
728 fn archived(bytes: &[u8]) -> &ArchivedValidatorsDiff {
729 rkyv::access::<ArchivedValidatorsDiff, rkyv::rancor::Error>(bytes)
730 .expect("test setup: failed to access archived delta")
731 }
732
733 #[derive(Clone, Debug, PartialEq, Eq)]
734 struct MockValidator {
735 withdrawal_credentials: [u8; 32],
736 effective_balance: u64,
737 slashed: bool,
738 activation_eligibility_epoch: u64,
739 activation_epoch: u64,
740 exit_epoch: u64,
741 withdrawable_epoch: u64,
742 }
743
744 impl MockValidator {
745 fn new(id: u8) -> Self {
746 Self {
747 withdrawal_credentials: [id; 32],
748 effective_balance: 32_000_000_000,
749 slashed: false,
750 activation_eligibility_epoch: 0,
751 activation_epoch: 0,
752 exit_epoch: 0,
753 withdrawable_epoch: 0,
754 }
755 }
756
757 fn to_ssz_bytes(&self) -> Vec<u8> {
759 let mut bytes = vec![0u8; VALIDATOR_SSZ_SIZE];
760 bytes[48..80].copy_from_slice(&self.withdrawal_credentials);
761 bytes[80..88].copy_from_slice(&self.effective_balance.to_le_bytes());
762 bytes[88] = self.slashed as u8;
763 bytes[89..97].copy_from_slice(&self.activation_eligibility_epoch.to_le_bytes());
764 bytes[97..105].copy_from_slice(&self.activation_epoch.to_le_bytes());
765 bytes[105..113].copy_from_slice(&self.exit_epoch.to_le_bytes());
766 bytes[113..121].copy_from_slice(&self.withdrawable_epoch.to_le_bytes());
767 bytes
768 }
769 }
770
771 impl ValidatorSnapshot for MockValidator {
772 fn withdrawal_credentials(&self) -> &[u8; 32] {
773 &self.withdrawal_credentials
774 }
775 fn effective_balance(&self) -> u64 {
776 self.effective_balance
777 }
778 fn is_slashed(&self) -> bool {
779 self.slashed
780 }
781 fn activation_eligibility_epoch(&self) -> u64 {
782 self.activation_eligibility_epoch
783 }
784 fn activation_epoch(&self) -> u64 {
785 self.activation_epoch
786 }
787 fn exit_epoch(&self) -> u64 {
788 self.exit_epoch
789 }
790 fn withdrawable_epoch(&self) -> u64 {
791 self.withdrawable_epoch
792 }
793 fn to_ssz_bytes(&self) -> Vec<u8> {
794 self.to_ssz_bytes()
795 }
796 }
797
798 struct MockMutValidator<'a>(&'a mut MockValidator);
800
801 impl<'a> ValidatorMut for MockMutValidator<'a> {
802 fn is_slashed(&self) -> bool {
803 self.0.slashed
804 }
805 fn set_withdrawal_credentials(&mut self, v: &[u8; 32]) {
806 self.0.withdrawal_credentials = *v;
807 }
808 fn set_effective_balance(&mut self, v: u64) {
809 self.0.effective_balance = v;
810 }
811 fn set_slashed(&mut self, v: bool) {
812 self.0.slashed = v;
813 }
814 fn set_activation_eligibility_epoch(&mut self, v: u64) {
815 self.0.activation_eligibility_epoch = v;
816 }
817 fn set_activation_epoch(&mut self, v: u64) {
818 self.0.activation_epoch = v;
819 }
820 fn set_exit_epoch(&mut self, v: u64) {
821 self.0.exit_epoch = v;
822 }
823 fn set_withdrawable_epoch(&mut self, v: u64) {
824 self.0.withdrawable_epoch = v;
825 }
826 }
827
828 struct MockValidatorTarget {
830 validators: Vec<MockValidator>,
831 }
832
833 impl ValidatorMutTarget for MockValidatorTarget {
834 type Validator<'a>
835 = MockMutValidator<'a>
836 where
837 Self: 'a;
838
839 fn get_mut(&mut self, index: usize) -> Option<Self::Validator<'_>> {
840 self.validators.get_mut(index).map(MockMutValidator)
841 }
842
843 fn push_from_ssz(&mut self, ssz_bytes: &[u8]) {
844 let wc = ssz_bytes
846 .get(48..80)
847 .and_then(|s| s.try_into().ok())
848 .expect("test setup: valid wc bytes");
849 let eb = u64::from_le_bytes(
850 ssz_bytes
851 .get(80..88)
852 .and_then(|s| s.try_into().ok())
853 .expect("test setup: valid eb bytes"),
854 );
855 let slashed = ssz_bytes.get(88).copied().expect("test setup") != 0;
856 let aee = u64::from_le_bytes(
857 ssz_bytes
858 .get(89..97)
859 .and_then(|s| s.try_into().ok())
860 .expect("test setup: valid aee bytes"),
861 );
862 let ae = u64::from_le_bytes(
863 ssz_bytes
864 .get(97..105)
865 .and_then(|s| s.try_into().ok())
866 .expect("test setup: valid ae bytes"),
867 );
868 let ee = u64::from_le_bytes(
869 ssz_bytes
870 .get(105..113)
871 .and_then(|s| s.try_into().ok())
872 .expect("test setup: valid ee bytes"),
873 );
874 let we = u64::from_le_bytes(
875 ssz_bytes
876 .get(113..121)
877 .and_then(|s| s.try_into().ok())
878 .expect("test setup: valid we bytes"),
879 );
880
881 self.validators.push(MockValidator {
882 withdrawal_credentials: wc,
883 effective_balance: eb,
884 slashed,
885 activation_eligibility_epoch: aee,
886 activation_epoch: ae,
887 exit_epoch: ee,
888 withdrawable_epoch: we,
889 });
890 }
891 }
892
893 #[test]
894 fn test_no_changes_iter() {
895 let base = vec![MockValidator::new(0), MockValidator::new(1)];
896 let target = base.clone();
897 let delta = diff_validators_iter(base.into_iter(), target.into_iter());
898 assert_eq!(delta.patches.len(), 0);
899 assert_eq!(delta.appended_validators.len(), 0);
900 }
901
902 #[test]
903 fn test_single_field_change_iter() {
904 let base = vec![MockValidator::new(0)];
905 let mut target = vec![MockValidator::new(0)];
906 target[0].effective_balance = 31_000_000_000;
907
908 let delta = diff_validators_iter(base.clone().into_iter(), target.clone().into_iter());
909 assert_eq!(delta.patches.len(), 1);
910 assert_eq!(delta.patches[0].field, ValidatorField::EffectiveBalance);
911 }
912
913 #[test]
914 fn test_appended_validators_iter() {
915 let base = vec![MockValidator::new(0)];
916 let target = vec![MockValidator::new(0), MockValidator::new(1)];
917
918 let delta = diff_validators_iter(base.into_iter(), target.into_iter());
919 assert_eq!(delta.patches.len(), 0);
920 assert_eq!(delta.appended_validators.len(), VALIDATOR_SSZ_SIZE);
921 }
922
923 #[test]
924 fn test_withdrawable_epoch_auto_reconstructed_non_slashed() {
925 let base = MockValidator::new(0);
926 let mut target = base.clone();
927
928 target.exit_epoch = 100;
929 target.withdrawable_epoch = 100 + MIN_VALIDATOR_WITHDRAWABILITY_DELAY;
930
931 let delta = diff_validators_iter(
932 std::iter::once(base.clone()),
933 std::iter::once(target.clone()),
934 );
935
936 assert_eq!(delta.patches.len(), 1);
938 assert_eq!(delta.patches[0].field, ValidatorField::ExitEpoch);
939
940 let bytes = archive(&delta);
941 let archived = archived(&bytes);
942
943 let mut state = MockValidatorTarget {
944 validators: vec![base],
945 };
946 apply_validators_iter(&mut state, archived).expect("test setup: apply failed");
947
948 assert_eq!(state.validators[0], target);
949 }
950
951 #[test]
952 fn test_withdrawable_epoch_explicit_patch_slashed() {
953 let mut base = MockValidator::new(0);
954 base.slashed = true;
955
956 let mut target = base.clone();
957 target.exit_epoch = 100;
958 target.withdrawable_epoch = 150; let delta = diff_validators_iter(
961 std::iter::once(base.clone()),
962 std::iter::once(target.clone()),
963 );
964
965 assert_eq!(delta.patches.len(), 2);
967 let fields: Vec<&ValidatorField> = delta.patches.iter().map(|p| &p.field).collect();
968 assert!(fields.contains(&&ValidatorField::ExitEpoch));
969 assert!(fields.contains(&&ValidatorField::WithdrawableEpochSlashed));
970
971 let bytes = archive(&delta);
972 let archived = archived(&bytes);
973
974 let mut state = MockValidatorTarget {
975 validators: vec![base],
976 };
977 apply_validators_iter(&mut state, archived).expect("test setup: apply failed");
978
979 assert_eq!(state.validators[0], target);
980 }
981
982 #[test]
983 fn test_byte_api_roundtrip() {
984 let base = [MockValidator::new(0), MockValidator::new(1)];
985 let target = [MockValidator::new(0), MockValidator::new(2)];
986
987 let base_bytes: Vec<u8> = base.iter().flat_map(|v| v.to_ssz_bytes()).collect();
988 let target_bytes: Vec<u8> = target.iter().flat_map(|v| v.to_ssz_bytes()).collect();
989
990 let delta = diff_validators(&base_bytes, &target_bytes);
991
992 let bytes = archive(&delta);
993 let archived = archived(&bytes);
994
995 let mut reconstructed = base_bytes;
996 apply_validators(&mut reconstructed, archived).expect("test setup: apply failed");
997
998 assert_eq!(reconstructed, target_bytes);
999 }
1000
1001 #[test]
1002 fn test_apply_out_of_bounds_index() {
1003 let base = vec![MockValidator::new(0)];
1004 let mut target = vec![MockValidator::new(0)];
1005 target[0].effective_balance = 100;
1006
1007 let delta = diff_validators_iter(base.into_iter(), target.into_iter());
1008 let bytes = archive(&delta);
1009 let archived = archived(&bytes);
1010
1011 let mut state = MockValidatorTarget { validators: vec![] };
1013
1014 let result = apply_validators_iter(&mut state, archived);
1015 assert!(result.is_err());
1016 let err_str = format!("{}", result.expect_err("test setup"));
1017 assert!(err_str.contains("out of bounds"));
1018 }
1019
1020 #[test]
1021 fn test_apply_invalid_patch_width() {
1022 let delta = ValidatorsDiff {
1024 patches: vec![ValidatorPatch {
1025 index: 0,
1026 field: ValidatorField::EffectiveBalance, value: vec![0; 4], }],
1029 appended_validators: vec![],
1030 };
1031
1032 let bytes = archive(&delta);
1033 let archived = archived(&bytes);
1034
1035 let mut state = MockValidatorTarget {
1036 validators: vec![MockValidator::new(0)],
1037 };
1038
1039 let result = apply_validators_iter(&mut state, archived);
1040 assert!(result.is_err());
1041 let err_str = format!("{}", result.expect_err("test setup"));
1042 assert!(
1043 err_str.contains("expected 8 bytes, got 4"),
1044 "Error message should mention invalid width"
1045 );
1046 }
1047
1048 #[test]
1049 fn test_apply_truncated_appended_validators() {
1050 let delta = ValidatorsDiff {
1052 patches: vec![],
1053 appended_validators: vec![0u8; VALIDATOR_SSZ_SIZE - 1], };
1055
1056 let bytes = archive(&delta);
1057 let archived = archived(&bytes);
1058
1059 let mut state = MockValidatorTarget { validators: vec![] };
1060
1061 let result = apply_validators_iter(&mut state, archived);
1062 assert!(result.is_err());
1063 let err_str = format!("{}", result.expect_err("test setup"));
1064 assert!(
1065 err_str.contains("does not contain complete SSZ records"),
1066 "Error message should mention truncated appended data"
1067 );
1068 }
1069}