Skip to main content

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}