eth_state_diff/balances.rs
1//! Compact delta encoding and reconstruction for Ethereum validator balances.
2//!
3//! This module computes compact binary deltas between two validator balance
4//! snapshots and applies those deltas to reconstruct the target snapshot.
5//!
6//! The encoding is specialized for Ethereum beacon-chain balances, where most
7//! validators either retain the same balance or change by a relatively small
8//! amount between consecutive states.
9//!
10//! ## Encoding
11//!
12//! For each balance in the portion shared by the base and target snapshots,
13//! the delta records one of four states using a packed two-bit tag:
14//!
15//! - [`SET_NO_CHANGE`] — the balance is unchanged;
16//! - [`SET_TO_ZERO`] — the target balance is zero;
17//! - [`SET_TO_DIFF`] — the target is reconstructed by applying a signed
18//! difference to the base balance;
19//! - [`SET_TO_TARGET_VALUE`] — the target balance is stored explicitly.
20//!
21//! Changed balances whose difference fits in an `i32` are normally encoded as
22//! signed differences. The most frequently occurring difference is selected as
23//! the [`BalancesDiff::mode`], and encoded differences store only the
24//! difference relative to that mode.
25//!
26//! Signed corrected differences are encoded as zig-zag integers followed by a
27//! variable-length integer encoding. This makes small and frequently occurring
28//! changes inexpensive to store.
29//!
30//! Differences that do not fit in an `i32` are encoded as explicit target
31//! values.
32//!
33//! Balances that exist only in the target snapshot are stored in
34//! [`BalancesDiff::appended_balances`].
35//!
36//! ## Two-pass encoding
37//!
38//! [`diff_balances_iter`] cannot require its input iterators to implement
39//! [`Clone`]. It therefore performs the diff in two logical passes:
40//!
41//! 1. the common portion of the iterators is consumed into a compact
42//! intermediate list of changes;
43//! 2. the statistical mode is selected and the changes are encoded.
44//!
45//! Any remaining items in the target iterator are treated as newly appended
46//! balances.
47//!
48//! ## Iterator API
49//!
50//! [`diff_balances`] is the convenience API for contiguous balance slices.
51//! [`diff_balances_iter`] is intended for consensus clients whose balances are
52//! stored in persistent lists, trees, or other non-contiguous structures.
53//!
54//! The iterator API avoids requiring the caller to materialize the complete
55//! balance registry as a flat buffer.
56//!
57//! ## Reconstruction
58//!
59//! [`apply_balances`] and [`apply_balances_iter`] mutate the supplied balance
60//! collection in place.
61//!
62//! The supplied collection must represent the **base snapshot** from which the
63//! delta was generated. After successful application, it contains the target
64//! balances.
65//!
66//! Existing balances are updated in place. Target balances that extend beyond
67//! the base snapshot are appended from the delta.
68//!
69//! ## Complexity
70//!
71//! [`diff_balances`] and [`diff_balances_iter`] run in:
72//!
73//! ```text
74//! O(n)
75//! ```
76//!
77//! where `n` is the number of balances in the common portion of the snapshots.
78//!
79//! Delta generation additionally requires storage proportional to the number
80//! of changed balances:
81//!
82//! ```text
83//! O(k)
84//! ```
85//!
86//! where `k` is the number of changed balances.
87//!
88//! [`apply_balances`] and [`apply_balances_iter`] run in:
89//!
90//! ```text
91//! O(n + a)
92//! ```
93//!
94//! where `n` is the number of balances represented by the tag vector and `a`
95//! is the number of appended balances.
96//!
97//! Reconstruction operates in place and does not require allocating a second
98//! balance buffer.
99//!
100//! ## Serialization
101//!
102//! [`BalancesDiff`] is designed to be serialized using `rkyv` and can then be
103//! passed to a general-purpose compressor such as zstd.
104//!
105//! The delta representation itself is independent of the serialization and
106//! compression layer.
107//!
108//! ## Delta validity
109//!
110//! Applying a delta assumes that the supplied base collection corresponds to
111//! the base snapshot used to create the delta. The application functions do
112//! not independently verify the original balance values.
113//!
114//! In particular, a `SET_TO_DIFF` entry applies its decoded difference to the
115//! current value in the supplied target collection. Applying the same delta to
116//! a different base snapshot therefore does not generally produce the intended
117//! target snapshot.
118//!
119//! [`BalancesDiff`]: crate::types::BalancesDiff
120//! [`SET_NO_CHANGE`]: crate::types::SET_NO_CHANGE
121//! [`SET_TO_ZERO`]: crate::types::SET_TO_ZERO
122//! [`SET_TO_DIFF`]: crate::types::SET_TO_DIFF
123//! [`SET_TO_TARGET_VALUE`]: crate::types::SET_TO_TARGET_VALUE
124
125use rustc_hash::FxHashMap;
126
127use crate::{
128 types::{
129 ArchivedBalancesDiff, BalancesDiff, BitTagVec, SET_NO_CHANGE, SET_TO_DIFF,
130 SET_TO_TARGET_VALUE, SET_TO_ZERO,
131 },
132 Error,
133};
134
135/// Computes a compact balance delta between two contiguous balance slices.
136///
137/// The returned [`BalancesDiff`] contains the information required to
138/// reconstruct `target` from `base`.
139///
140/// The common portion of the two slices is encoded using packed two-bit tags,
141/// statistical mode correction, zig-zag encoded signed differences, and
142/// explicit target values where necessary. If `target` contains more balances
143/// than `base`, the additional balances are stored in
144/// [`BalancesDiff::appended_balances`].
145///
146/// This is a convenience wrapper around [`diff_balances_iter`].
147///
148/// # Complexity
149///
150/// `O(n)` time and `O(k)` additional space, where `n` is the number of balances
151/// in the common portion and `k` is the number of changed balances.
152///
153/// # Examples
154///
155/// ```ignore
156/// let delta = diff_balances(&base, &target);
157/// ```
158///
159/// [`BalancesDiff`]: crate::types::BalancesDiff
160/// [`diff_balances_iter`]: crate::balances::diff_balances_iter
161pub fn diff_balances(base: &[u64], target: &[u64]) -> BalancesDiff {
162 diff_balances_iter(base.iter().copied(), target.iter().copied())
163}
164
165/// Computes a compact balance delta between two balance iterators.
166///
167/// This is the generic counterpart to [`diff_balances`]. It is intended for
168/// consensus clients whose balance storage is not represented as a contiguous
169/// `&[u64]`.
170///
171/// The iterators must implement [`ExactSizeIterator`], allowing the function
172/// to determine the size of the common portion and distinguish existing
173/// balances from balances appended to the target.
174///
175/// The iterators are consumed during encoding and do not need to implement
176/// [`Clone`].
177///
178/// The common portion is first collected into a compact list of changed
179/// balances so that the most frequently occurring balance difference can be
180/// selected as the encoding mode. Remaining items in the target iterator are
181/// stored as appended balances.
182///
183/// # Complexity
184///
185/// `O(n)` time and `O(k + a)` additional space, where:
186///
187/// - `n` is the number of balances in the common portion;
188/// - `k` is the number of changed balances; and
189/// - `a` is the number of balances appended to the target.
190///
191/// [`diff_balances`]: crate::balances::diff_balances
192pub fn diff_balances_iter<I1, I2>(mut base: I1, mut target: I2) -> BalancesDiff
193where
194 I1: ExactSizeIterator<Item = u64>,
195 I2: ExactSizeIterator<Item = u64>,
196{
197 let common_len = base.len().min(target.len());
198 let mut changes = Vec::with_capacity(1024);
199
200 for idx in 0..common_len {
201 let Some(v1) = base.next() else {
202 break;
203 };
204 let Some(v2) = target.next() else {
205 break;
206 };
207
208 if v1 != v2 {
209 let diff = v2
210 .checked_sub(v1)
211 .and_then(|value| i64::try_from(value).ok())
212 .or_else(|| {
213 v1.checked_sub(v2)
214 .and_then(|value| i64::try_from(value).ok())
215 .map(|value| -value)
216 });
217
218 changes.push(Change {
219 idx,
220 diff,
221 target: v2,
222 });
223 }
224 }
225
226 let mode = find_mode(&changes);
227 let (tags, varint_payload, target_values) = encode(common_len, &changes, mode);
228
229 BalancesDiff {
230 tags,
231 mode,
232 varint_payload,
233 target_values,
234 appended_balances: target.collect(),
235 }
236}
237
238/// Applies a balance delta to a mutable balance collection in place.
239///
240/// `target` must initially contain the **base balance snapshot** used to
241/// generate `delta`.
242///
243/// Existing balances are reconstructed according to the packed tags in the
244/// delta. Changed balances encoded as differences are updated relative to
245/// their current base value, while explicit and zero-valued entries replace
246/// the corresponding balance directly.
247///
248/// Any balances stored in [`BalancesDiff::appended_balances`] are appended to
249/// the collection after the common portion has been reconstructed.
250///
251/// This API accepts [`crate::ListMutTarget`] so that consensus clients can
252/// apply the delta directly to tree-backed or persistent balance collections
253/// without first materializing them as a `Vec<u64>`.
254///
255/// # Errors
256///
257/// Returns [`Error::MalformedDelta`] if the delta is structurally invalid,
258/// contains incomplete payload data, or cannot be safely applied to the
259/// supplied target collection.
260pub fn apply_balances_iter<T: crate::ListMutTarget<u64>>(
261 target: &mut T,
262 delta: &ArchivedBalancesDiff,
263) -> Result<(), Error> {
264 let mode = delta.mode.to_native();
265
266 let tag_len = usize::try_from(delta.tags.len.to_native())
267 .map_err(|_| Error::MalformedDelta("delta tag length does not fit in usize".into()))?;
268
269 if target.len() != tag_len {
270 return Err(Error::MalformedDelta(format!(
271 "target length {} does not match delta tag length {tag_len}",
272 target.len()
273 )));
274 }
275
276 let mut target_iter = delta.target_values.iter();
277 let payload = delta.varint_payload.as_slice();
278 let mut payload_cursor = 0usize;
279 let mut base_idx = 0usize;
280
281 for &tag_byte in delta.tags.data.iter() {
282 if base_idx >= tag_len {
283 break;
284 }
285
286 // Fast path: four consecutive SET_NO_CHANGE entries.
287 if tag_byte == 0 {
288 base_idx = (base_idx + 4).min(tag_len);
289 continue;
290 }
291
292 for bit in 0..4 {
293 if base_idx >= tag_len {
294 break;
295 }
296
297 let tag = (tag_byte >> (bit * 2)) & 0b11;
298
299 match tag {
300 SET_NO_CHANGE => {}
301
302 SET_TO_ZERO => {
303 let Some(value) = target.get_mut(base_idx) else {
304 return Err(Error::MalformedDelta(format!(
305 "target collection is missing balance at index {base_idx}"
306 )));
307 };
308
309 *value = 0;
310 }
311
312 SET_TO_TARGET_VALUE => {
313 let Some(target_value) = target_iter.next() else {
314 return Err(Error::MalformedDelta(
315 "target-value payload is shorter than the encoded target-value tags"
316 .into(),
317 ));
318 };
319
320 let Some(value) = target.get_mut(base_idx) else {
321 return Err(Error::MalformedDelta(format!(
322 "target collection is missing balance at index {base_idx}"
323 )));
324 };
325
326 *value = target_value.to_native();
327 }
328
329 SET_TO_DIFF => {
330 let encoded = read_varint(payload, &mut payload_cursor)?;
331 let corrected = zigzag_decode(encoded);
332
333 let Some(diff) = corrected.checked_add(mode) else {
334 return Err(Error::MalformedDelta(
335 "decoded balance difference overflows i64".into(),
336 ));
337 };
338
339 let Some(value) = target.get_mut(base_idx) else {
340 return Err(Error::MalformedDelta(format!(
341 "target collection is missing balance at index {base_idx}"
342 )));
343 };
344
345 let base_value = i64::try_from(*value).map_err(|_| {
346 Error::MalformedDelta(format!(
347 "base balance at index {base_idx} exceeds i64 range"
348 ))
349 })?;
350
351 let updated = base_value
352 .checked_add(diff)
353 .and_then(|value| u64::try_from(value).ok())
354 .ok_or_else(|| {
355 Error::MalformedDelta(format!(
356 "decoded balance difference produces an invalid value at index {base_idx}"
357 ))
358 })?;
359
360 *value = updated;
361 }
362
363 _ => {
364 return Err(Error::MalformedDelta(format!(
365 "invalid balance tag {tag} at index {base_idx}"
366 )));
367 }
368 }
369
370 base_idx += 1;
371 }
372 }
373
374 if !delta.appended_balances.is_empty() {
375 for value in delta.appended_balances.iter() {
376 target.push(value.to_native());
377 }
378 }
379
380 Ok(())
381}
382
383/// Applies a balance delta to a contiguous balance vector in place.
384///
385/// `base` must initially contain the balance snapshot from which `delta` was
386/// generated. After successful application, `base` contains the reconstructed
387/// target snapshot.
388///
389/// This is a convenience wrapper around [`apply_balances_iter`] for clients
390/// that store balances contiguously.
391///
392/// # Errors
393///
394/// Returns an error if `delta` is malformed or if its represented base length
395/// does not match `base`.
396///
397/// [`apply_balances_iter`]: crate::balances::apply_balances_iter
398pub fn apply_balances(base: &mut Vec<u64>, delta: &ArchivedBalancesDiff) -> Result<(), Error> {
399 apply_balances_iter(base, delta)
400}
401
402/// Intermediate representation of a changed balance.
403///
404/// This is retained between the comparison pass and encoding pass so that
405/// `diff_balances_iter` can determine the statistical mode without requiring
406/// the source iterators to be cloned.
407struct Change {
408 idx: usize,
409 diff: Option<i64>,
410 target: u64,
411}
412
413/// Finds the most frequently occurring representable balance difference.
414///
415/// Only differences that fit within `i32` participate in mode selection.
416/// Larger differences are always encoded as explicit target values and
417/// therefore do not benefit from mode correction.
418///
419/// Returns `0` when no representable balance difference exists.
420fn find_mode(changes: &[Change]) -> i64 {
421 let mut freq_map = FxHashMap::default();
422 freq_map.reserve(256);
423
424 for change in changes {
425 let Some(diff) = change.diff else {
426 continue;
427 };
428
429 if i32::try_from(diff).is_ok() {
430 *freq_map.entry(diff).or_insert(0usize) += 1;
431 }
432 }
433
434 freq_map
435 .into_iter()
436 .max_by_key(|&(_, count)| count)
437 .map(|(value, _)| value)
438 .unwrap_or(0)
439}
440
441/// Encodes changed balances using the selected statistical mode.
442///
443/// Each changed balance is assigned a two-bit tag. Differences that fit in
444/// `i32` are encoded as zig-zag varints after subtracting `mode`; values that
445/// cannot use the difference representation are stored as absolute target
446/// values.
447fn encode(common_len: usize, changes: &[Change], mode: i64) -> (BitTagVec, Vec<u8>, Vec<u64>) {
448 let mut tags = BitTagVec::new(common_len);
449 let mut varint_payload = Vec::with_capacity(changes.len());
450 let mut target_values = Vec::new();
451
452 for change in changes {
453 let Change { idx, diff, target } = *change;
454
455 if target == 0 {
456 tags.set(idx, SET_TO_ZERO);
457 continue;
458 }
459
460 let Some(diff) = diff else {
461 tags.set(idx, SET_TO_TARGET_VALUE);
462 target_values.push(target);
463 continue;
464 };
465
466 if i32::try_from(diff).is_ok() {
467 tags.set(idx, SET_TO_DIFF);
468
469 // `diff` and `mode` are both representable i32 values, so their
470 // difference is within the i64 range.
471 let corrected = diff - mode;
472 write_varint(zigzag_encode(corrected), &mut varint_payload);
473 } else {
474 tags.set(idx, SET_TO_TARGET_VALUE);
475 target_values.push(target);
476 }
477 }
478
479 (tags, varint_payload, target_values)
480}
481
482/// Encodes a signed integer using zig-zag encoding.
483///
484/// Negative and positive values of similar magnitude are mapped to nearby
485/// unsigned values, making small signed differences efficient to represent
486/// with the subsequent variable-length encoding.
487#[inline]
488fn zigzag_encode(n: i64) -> u64 {
489 ((n as u64) << 1) ^ ((n >> 63) as u64)
490}
491
492/// Decodes a value previously encoded with [`zigzag_encode`].
493#[inline]
494fn zigzag_decode(n: u64) -> i64 {
495 ((n >> 1) as i64) ^ -((n & 1) as i64)
496}
497
498/// Encodes an unsigned integer using the variable-length format used by the
499/// balance delta representation.
500///
501/// Each output byte stores seven payload bits. The high bit indicates whether
502/// another byte follows.
503#[inline]
504pub(super) fn write_varint(mut val: u64, buf: &mut Vec<u8>) {
505 loop {
506 if val < 0x80 {
507 buf.push(val as u8);
508 break;
509 }
510
511 buf.push((val as u8) | 0x80);
512 val >>= 7;
513 }
514}
515
516/// Decodes one unsigned variable-length integer from `buf`.
517///
518/// `cursor` is advanced past the decoded integer.
519///
520/// Returns an error if the payload is truncated or contains more than ten
521/// bytes, or if the final byte contains bits outside the `u64` range.
522#[inline]
523pub(super) fn read_varint(buf: &[u8], cursor: &mut usize) -> Result<u64, Error> {
524 let mut value = 0u64;
525
526 for shift in (0..=63).step_by(7) {
527 let Some(&byte) = buf.get(*cursor) else {
528 return Err(Error::MalformedDelta(format!(
529 "truncated varint payload at byte offset {}",
530 *cursor
531 )));
532 };
533
534 *cursor += 1;
535
536 let payload = (byte & 0x7f) as u64;
537
538 if shift == 63 && payload > 1 {
539 return Err(Error::MalformedDelta(format!(
540 "varint overflows u64 at byte offset {}",
541 *cursor - 1
542 )));
543 }
544
545 value |= payload << shift;
546
547 if byte & 0x80 == 0 {
548 return Ok(value);
549 }
550 }
551
552 Err(Error::MalformedDelta(
553 "varint exceeds the maximum length for a u64".into(),
554 ))
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use crate::types::ArchivedBalancesDiff;
561 use crate::ListMutTarget;
562
563 /// A minimal mock target for testing `apply_balances_iter` directly.
564 struct MockTarget {
565 inner: Vec<u64>,
566 }
567
568 impl ListMutTarget<u64> for MockTarget {
569 fn len(&self) -> usize {
570 self.inner.len()
571 }
572
573 fn get_mut(&mut self, index: usize) -> Option<&mut u64> {
574 self.inner.get_mut(index)
575 }
576
577 fn push(&mut self, value: u64) {
578 self.inner.push(value);
579 }
580 }
581
582 /// Helper to perform a full roundtrip: diff -> rkyv serialize -> rkyv access -> apply
583 fn assert_roundtrip(base: &[u64], target: &[u64]) {
584 let delta = diff_balances(base, target);
585
586 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta)
587 .expect("test setup: failed to serialize delta");
588
589 let archived = rkyv::access::<ArchivedBalancesDiff, rkyv::rancor::Error>(&bytes)
590 .expect("test setup: failed to access archived delta");
591
592 let mut reconstructed = base.to_vec();
593 apply_balances(&mut reconstructed, archived)
594 .expect("test setup: failed to apply valid delta");
595
596 assert_eq!(
597 reconstructed, target,
598 "Roundtrip failed: base={base:?}, target={target:?}, delta={delta:?}"
599 );
600 }
601
602 #[test]
603 fn test_no_changes() {
604 let state = vec![1000; 10];
605 assert_roundtrip(&state, &state);
606 }
607
608 #[test]
609 fn test_single_balance_change() {
610 let base = vec![100, 200, 300];
611 let target = vec![100, 205, 300];
612 assert_roundtrip(&base, &target);
613 }
614
615 #[test]
616 fn test_all_set_to_zero() {
617 let base = vec![100, 200, 300];
618 let target = vec![0, 0, 0];
619 assert_roundtrip(&base, &target);
620 }
621
622 #[test]
623 fn test_appended_balances() {
624 let base = vec![100, 200];
625 let target = vec![100, 200, 300, 400];
626 assert_roundtrip(&base, &target);
627 }
628
629 #[test]
630 fn test_only_appended_balances() {
631 let base = vec![];
632 let target = vec![10, 20, 30];
633 assert_roundtrip(&base, &target);
634 }
635
636 #[test]
637 fn test_mode_selection_frequent_diff() {
638 // 1000 balances. 500 of them change by exactly +1000.
639 // Mode should be 1000, making those diffs very cheap to encode.
640 let base: Vec<u64> = (0..1000).map(|i| (i * 100) as u64).collect();
641 let mut target = base.clone();
642
643 for i in (0..1000).step_by(2) {
644 target[i] += 1000;
645 }
646
647 let delta = diff_balances(&base, &target);
648 assert_eq!(
649 delta.mode, 1000,
650 "Mode should select the most frequent difference"
651 );
652 assert_roundtrip(&base, &target);
653 }
654
655 #[test]
656 fn test_i32_boundary_max_diff() {
657 let base = vec![100];
658 // diff = i32::MAX = 2147483647
659 let target = vec![100 + i32::MAX as u64];
660 assert_roundtrip(&base, &target);
661 }
662
663 #[test]
664 fn test_i32_boundary_min_diff() {
665 // diff = -2147483647 (which is i32::MIN + 1, fits in i32)
666 let base = vec![100 + 2147483647];
667 let target = vec![100];
668 assert_roundtrip(&base, &target);
669 }
670
671 #[test]
672 fn test_i32_overflow_uses_target_value() {
673 // diff = i32::MAX + 1 = 2147483648 (does NOT fit in i32)
674 let base = vec![100];
675 let target = vec![100 + i32::MAX as u64 + 1];
676
677 let delta = diff_balances(&base, &target);
678
679 // It must fall back to SET_TO_TARGET_VALUE, which means no varint payload
680 // should be generated for this diff.
681 assert_eq!(
682 delta.target_values.len(),
683 1,
684 "u64 diff exceeding i32::MAX must use SET_TO_TARGET_VALUE"
685 );
686 assert_eq!(
687 delta.varint_payload.len(),
688 0,
689 "Should not use varint payload for unrepresentable diff"
690 );
691
692 assert_roundtrip(&base, &target);
693 }
694
695 #[test]
696 fn test_u64_extreme_values_use_target_value() {
697 // base = 0, target = u64::MAX. Cannot be encoded as a diff.
698 let base = vec![0];
699 let target = vec![u64::MAX];
700 let delta = diff_balances(&base, &target);
701 assert_eq!(
702 delta.target_values.len(),
703 1,
704 "u64::MAX diff must use SET_TO_TARGET_VALUE"
705 );
706 assert_eq!(
707 delta.varint_payload.len(),
708 0,
709 "Should not use varint payload for unrepresentable diff"
710 );
711
712 // base = u64::MAX, target = 0. Diff is unrepresentable, but the encoder
713 // optimizes this to SET_TO_ZERO before it even considers the diff.
714 // We just verify it roundtrips correctly.
715 assert_roundtrip(&[0], &[u64::MAX]);
716 assert_roundtrip(&[u64::MAX], &[0]);
717 }
718
719 #[test]
720 fn test_apply_length_mismatch_returns_error() {
721 let base = vec![100, 200];
722 let target = vec![100, 200];
723
724 let delta = diff_balances(&base, &target);
725 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta)
726 .expect("test setup: failed to serialize delta");
727 let archived = rkyv::access::<ArchivedBalancesDiff, rkyv::rancor::Error>(&bytes)
728 .expect("test setup: failed to access archived delta");
729
730 let mut wrong_base = MockTarget { inner: vec![100] }; // Length 1 instead of 2
731 let result = apply_balances_iter(&mut wrong_base, archived);
732
733 assert!(result.is_err(), "Should error on length mismatch");
734 let err_str = format!("{}", result.expect_err("test setup: expected error"));
735 assert!(
736 err_str.contains("does not match delta tag length"),
737 "Error message should mention length mismatch"
738 );
739 }
740
741 #[test]
742 fn test_apply_i64_overflow_returns_error() {
743 // Setup a valid delta where mode = 1, and it applies a diff of 1.
744 let base = vec![100, 200];
745 let target = vec![101, 201];
746 let delta = diff_balances(&base, &target);
747 assert_eq!(delta.mode, 1);
748
749 let mut bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta)
750 .expect("test setup: failed to serialize delta");
751
752 // Find the mode (i64 = 1) and maliciously patch it to i64::MAX
753 let mode_bytes = 1i64.to_le_bytes();
754 let max_bytes = i64::MAX.to_le_bytes();
755
756 let pos = bytes
757 .windows(8)
758 .position(|w| w == mode_bytes)
759 .expect("test setup: could not find mode in serialized delta");
760
761 bytes
762 .get_mut(pos..pos + 8)
763 .expect("test setup: `pos` is valid and derived from a window of length 8")
764 .copy_from_slice(&max_bytes);
765
766 let archived = rkyv::access::<ArchivedBalancesDiff, rkyv::rancor::Error>(&bytes)
767 .expect("test setup: failed to access archived delta");
768 let mut state = MockTarget { inner: base };
769
770 // Patching mode to i64::MAX means the decoded diff becomes i64::MAX.
771 // Applying i64::MAX to a base of 100 overflows during the final u64 conversion.
772 let result = apply_balances_iter(&mut state, archived);
773 assert!(result.is_err(), "Should error on overflow during apply");
774 let err_str = format!("{}", result.expect_err("test setup"));
775 assert!(
776 err_str.contains("produces an invalid value"),
777 "Error message should mention the invalid resulting value"
778 );
779 }
780
781 #[test]
782 fn test_apply_base_exceeds_i64_range_returns_error() {
783 // Base balance is i64::MAX + 1. Even a diff of 0 (no change) wouldn't fail,
784 // but applying a diff of 1 requires reading base as i64, which will fail.
785 let base = vec![i64::MAX as u64 + 1];
786 let target = vec![i64::MAX as u64 + 2]; // diff is 1
787
788 let delta = diff_balances(&base, &target);
789
790 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&delta)
791 .expect("test setup: failed to serialize delta");
792 let archived = rkyv::access::<ArchivedBalancesDiff, rkyv::rancor::Error>(&bytes)
793 .expect("test setup: failed to access archived delta");
794
795 let mut state = MockTarget { inner: base };
796
797 let result = apply_balances_iter(&mut state, archived);
798 assert!(result.is_err());
799 let err_str = format!("{}", result.expect_err("test setup"));
800 assert!(
801 err_str.contains("exceeds i64 range"),
802 "Error message should mention base balance exceeding i64"
803 );
804 }
805
806 #[test]
807 fn test_zigzag_roundtrip() {
808 let values = [
809 0i64,
810 1,
811 -1,
812 2,
813 -2,
814 i32::MAX as i64,
815 i32::MIN as i64,
816 i64::MAX,
817 i64::MIN,
818 ];
819 for &v in &values {
820 assert_eq!(zigzag_decode(zigzag_encode(v)), v);
821 }
822 }
823
824 #[test]
825 fn test_write_and_read_varint_roundtrip() {
826 let values = [0u64, 1, 127, 128, 255, 16383, 16384, u64::MAX];
827 for &v in &values {
828 let mut buf = Vec::new();
829 write_varint(v, &mut buf);
830 let mut cursor = 0;
831 let decoded = read_varint(&buf, &mut cursor).expect("valid write");
832 assert_eq!(decoded, v);
833 assert_eq!(cursor, buf.len());
834 }
835 }
836
837 #[test]
838 fn test_read_varint_zero() {
839 let buf = [0u8];
840 let mut cursor = 0;
841 assert_eq!(read_varint(&buf, &mut cursor).expect("valid zero"), 0);
842 assert_eq!(cursor, 1);
843 }
844
845 #[test]
846 fn test_read_varint_two_bytes() {
847 // 128 = 0x80. Encoded as: 0x80 (low 7 bits = 0, continuation), 0x01 (low 7 bits = 1).
848 let buf = [0x80, 0x01];
849 let mut cursor = 0;
850 assert_eq!(read_varint(&buf, &mut cursor).expect("valid 128"), 128);
851 assert_eq!(cursor, 2);
852 }
853
854 #[test]
855 fn test_read_varint_max_u64() {
856 // u64::MAX encoded as 10 bytes
857 let buf: [u8; 10] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x01];
858 let mut cursor = 0;
859 assert_eq!(read_varint(&buf, &mut cursor).expect("valid max"), u64::MAX);
860 assert_eq!(cursor, 10);
861 }
862
863 #[test]
864 fn test_read_varint_overflow_u64() {
865 // Attempt to encode u64::MAX + 1 (bit 64 set).
866 // Same as max, but last byte has payload 2 instead of 1.
867 let buf: [u8; 10] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x02];
868 let mut cursor = 0;
869 let result = read_varint(&buf, &mut cursor);
870 assert!(result.is_err());
871 assert!(format!("{}", result.expect_err("test setup")).contains("overflows u64"));
872 }
873
874 #[test]
875 fn test_read_varint_truncated() {
876 let buf = [0xFF]; // Indicates continuation, but buffer ends
877 let mut cursor = 0;
878 let result = read_varint(&buf, &mut cursor);
879 assert!(result.is_err());
880 assert!(format!("{}", result.expect_err("test setup")).contains("truncated"));
881 }
882
883 #[test]
884 fn test_read_varint_exceeds_max_length() {
885 // 9 bytes of 0xFF, followed by 0x81.
886 // 0x81 has a valid payload (1), but the continuation bit is set,
887 // forcing the loop to attempt an 11th byte which exceeds u64 max length.
888 let buf: [u8; 10] = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81];
889 let mut cursor = 0;
890 let result = read_varint(&buf, &mut cursor);
891 assert!(result.is_err());
892 assert!(
893 format!("{}", result.expect_err("test setup")).contains("exceeds the maximum length")
894 );
895 }
896}