Skip to main content

linera_views/views/
historical_hash_wrapper.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{
5    marker::PhantomData,
6    ops::{Deref, DerefMut},
7    sync::Mutex,
8};
9
10use allocative::Allocative;
11#[cfg(with_metrics)]
12use linera_base::prometheus_util::MeasureLatency as _;
13use linera_base::visit_allocative_simple;
14
15use crate::{
16    batch::Batch,
17    common::from_bytes_option,
18    context::Context,
19    store::{ReadableKeyValueStore as _, WritableKeyValueStore as _},
20    views::{ClonableView, Hasher, HasherOutput, ReplaceContext, View, ViewError, MIN_VIEW_TAG},
21};
22
23#[cfg(with_metrics)]
24pub(crate) mod metrics {
25    use linera_base::prometheus_util::{exponential_bucket_latencies, register_histogram_vec};
26    use prometheus::HistogramVec;
27
28    linera_base::declare_metrics! {
29        /// The runtime of hash computation
30        pub static HISTORICALLY_HASHABLE_VIEW_HASH_RUNTIME: HistogramVec =
31            register_histogram_vec(
32                "historically_hashable_view_hash_runtime",
33                "HistoricallyHashableView hash runtime",
34                &[],
35                exponential_bucket_latencies(5.0),
36            );
37    }
38}
39
40/// Wrapper to compute the hash of the view based on its history of modifications.
41#[derive(Debug, Allocative)]
42#[allocative(bound = "C, W: Allocative")]
43pub struct HistoricallyHashableView<C, W> {
44    /// The hash in storage.
45    #[allocative(visit = visit_allocative_simple)]
46    stored_hash: Option<HasherOutput>,
47    /// The inner view.
48    inner: W,
49    /// Memoized hash, if any.
50    #[allocative(visit = visit_allocative_simple)]
51    hash: Mutex<Option<HasherOutput>>,
52    /// An override hash scheduled by [`Self::dump_content`]. While this is `Some`, the next
53    /// save records this value as the new stored hash without mixing in the pending inner
54    /// batch. Always derived from the canonical byte representation of the view's content,
55    /// never from a caller-supplied value.
56    #[allocative(visit = visit_allocative_simple)]
57    force_stored_hash: Option<HasherOutput>,
58    /// Track context type.
59    #[allocative(skip)]
60    _phantom: PhantomData<C>,
61}
62
63/// Key tags to create the sub-keys of a `HistoricallyHashableView` on top of the base key.
64#[repr(u8)]
65enum KeyTag {
66    /// Prefix for the indices of the view.
67    Inner = MIN_VIEW_TAG,
68    /// Prefix for the hash.
69    Hash,
70}
71
72impl<C, W> HistoricallyHashableView<C, W> {
73    fn make_hash(
74        stored_hash: Option<HasherOutput>,
75        batch: &Batch,
76    ) -> Result<HasherOutput, ViewError> {
77        #[cfg(with_metrics)]
78        let _hash_latency = metrics::HISTORICALLY_HASHABLE_VIEW_HASH_RUNTIME.measure_latency();
79        let stored_hash = stored_hash.unwrap_or_default();
80        if batch.is_empty() {
81            return Ok(stored_hash);
82        }
83        let mut hasher = sha3::Sha3_256::default();
84        hasher.update_with_bytes(&stored_hash)?;
85        hasher.update_with_bcs_bytes(&batch)?;
86        Ok(hasher.finalize())
87    }
88}
89
90impl<C, W, C2> ReplaceContext<C2> for HistoricallyHashableView<C, W>
91where
92    W: View<Context = C> + ReplaceContext<C2>,
93    C: Context,
94    C2: Context,
95{
96    type Target = HistoricallyHashableView<C2, <W as ReplaceContext<C2>>::Target>;
97
98    async fn with_context(
99        &mut self,
100        ctx: impl FnOnce(&Self::Context) -> C2 + Clone,
101    ) -> Self::Target {
102        HistoricallyHashableView {
103            _phantom: PhantomData,
104            stored_hash: self.stored_hash,
105            hash: Mutex::new(*self.hash.get_mut().unwrap()),
106            force_stored_hash: self.force_stored_hash,
107            inner: self.inner.with_context(ctx).await,
108        }
109    }
110}
111
112impl<W> View for HistoricallyHashableView<W::Context, W>
113where
114    W: View,
115{
116    const NUM_INIT_KEYS: usize = 1 + W::NUM_INIT_KEYS;
117
118    type Context = W::Context;
119
120    fn context(&self) -> Self::Context {
121        // The inner context has our base key plus the KeyTag::Inner byte
122        self.inner.context().clone_with_trimmed_key(1)
123    }
124
125    fn pre_load(context: &Self::Context) -> Result<Vec<Vec<u8>>, ViewError> {
126        let mut v = vec![context.base_key().base_tag(KeyTag::Hash as u8)];
127        let base_key = context.base_key().base_tag(KeyTag::Inner as u8);
128        let context = context.clone_with_base_key(base_key);
129        v.extend(W::pre_load(&context)?);
130        Ok(v)
131    }
132
133    fn post_load(context: Self::Context, values: &[Option<Vec<u8>>]) -> Result<Self, ViewError> {
134        let hash = from_bytes_option(values.first().ok_or(ViewError::PostLoadValuesError)?)?;
135        let base_key = context.base_key().base_tag(KeyTag::Inner as u8);
136        let context = context.clone_with_base_key(base_key);
137        let inner = W::post_load(
138            context,
139            values.get(1..).ok_or(ViewError::PostLoadValuesError)?,
140        )?;
141        Ok(Self {
142            _phantom: PhantomData,
143            stored_hash: hash,
144            hash: Mutex::new(hash),
145            force_stored_hash: None,
146            inner,
147        })
148    }
149
150    async fn load(context: Self::Context) -> Result<Self, ViewError> {
151        let keys = Self::pre_load(&context)?;
152        let values = context.store().read_multi_values_bytes(&keys).await?;
153        Self::post_load(context, &values)
154    }
155
156    fn rollback(&mut self) {
157        self.inner.rollback();
158        *self.hash.get_mut().unwrap() = self.stored_hash;
159        self.force_stored_hash = None;
160    }
161
162    async fn has_pending_changes(&self) -> bool {
163        self.force_stored_hash.is_some() || self.inner.has_pending_changes().await
164    }
165
166    fn pre_save(&self, batch: &mut Batch) -> Result<bool, ViewError> {
167        let mut inner_batch = Batch::new();
168        self.inner.pre_save(&mut inner_batch)?;
169        let new_hash = {
170            let mut maybe_hash = self.hash.lock().unwrap();
171            if let Some(forced) = self.force_stored_hash {
172                // The override pre-empts the hash chain: the inner batch is still written
173                // to storage, but it does not contribute to the hash.
174                *maybe_hash = Some(forced);
175                forced
176            } else {
177                match maybe_hash.as_mut() {
178                    Some(hash) => *hash,
179                    None => {
180                        let hash = Self::make_hash(self.stored_hash, &inner_batch)?;
181                        *maybe_hash = Some(hash);
182                        hash
183                    }
184                }
185            }
186        };
187        batch.operations.extend(inner_batch.operations);
188
189        if self.stored_hash != Some(new_hash) {
190            let mut key = self.inner.context().base_key().bytes.clone();
191            let tag = key.last_mut().unwrap();
192            *tag = KeyTag::Hash as u8;
193            batch.put_key_value(key, &new_hash)?;
194        }
195        // Never delete the stored hash, even if the inner view was cleared.
196        Ok(false)
197    }
198
199    fn post_save(&mut self) {
200        let new_hash = self
201            .hash
202            .get_mut()
203            .unwrap()
204            .expect("hash should be computed in pre_save");
205        self.stored_hash = Some(new_hash);
206        self.force_stored_hash = None;
207        self.inner.post_save();
208    }
209
210    fn clear(&mut self) {
211        self.inner.clear();
212        *self.hash.get_mut().unwrap() = None;
213        self.force_stored_hash = None;
214    }
215}
216
217impl<W> ClonableView for HistoricallyHashableView<W::Context, W>
218where
219    W: ClonableView,
220{
221    fn clone_unchecked(&mut self) -> Result<Self, ViewError> {
222        Ok(HistoricallyHashableView {
223            _phantom: PhantomData,
224            stored_hash: self.stored_hash,
225            hash: Mutex::new(*self.hash.get_mut().unwrap()),
226            force_stored_hash: self.force_stored_hash,
227            inner: self.inner.clone_unchecked()?,
228        })
229    }
230}
231
232impl<W: View> HistoricallyHashableView<W::Context, W> {
233    /// Obtains a hash of the history of the changes in the view.
234    pub async fn historical_hash(&mut self) -> Result<HasherOutput, ViewError> {
235        if let Some(forced) = self.force_stored_hash {
236            return Ok(forced);
237        }
238        if let Some(hash) = self.hash.get_mut().unwrap() {
239            return Ok(*hash);
240        }
241        let mut batch = Batch::new();
242        self.inner.pre_save(&mut batch)?;
243        let hash = Self::make_hash(self.stored_hash, &batch)?;
244        // Remember the hash that we just computed.
245        *self.hash.get_mut().unwrap() = Some(hash);
246        Ok(hash)
247    }
248
249    /// Returns the canonical byte representation of the inner view's persisted content
250    /// and arranges for the next save to record the hash of those bytes as the new
251    /// stored hash. Subsequent updates extend the history from that hash normally.
252    ///
253    /// The bytes are the BCS encoding of `Vec<(Vec<u8>, Vec<u8>)>` — every entry stored
254    /// under the inner view's prefix, in sorted lexicographic key order. Two views with
255    /// identical persisted content produce identical bytes by construction; the hash is
256    /// therefore reproducible by any party holding the bytes.
257    ///
258    /// Errors with [`ViewError::HasPendingChanges`] if the inner view has unflushed
259    /// changes — the dump reads from the underlying KV store and would silently miss
260    /// in-memory modifications.
261    pub async fn dump_content(&mut self) -> Result<(Vec<u8>, HasherOutput), ViewError> {
262        if self.inner.has_pending_changes().await {
263            return Err(ViewError::HasPendingChanges);
264        }
265        let context = self.inner.context();
266        // The inner context's base key is `<wrapper_base><Inner>`; passing it as the
267        // search prefix scopes the dump to the inner view's data and excludes the
268        // wrapper's hash key, which lives at `<wrapper_base><Hash>`.
269        let inner_prefix = context.base_key().bytes.clone();
270        let key_values = context
271            .store()
272            .find_key_values_by_prefix(&inner_prefix)
273            .await
274            .map_err(|err| ViewError::StoreError {
275                backend: "HistoricallyHashableView::dump_content",
276                error: Box::new(err),
277                must_reload_view: false,
278            })?;
279        let bytes = bcs::to_bytes(&key_values)?;
280        let hash = hash_bytes(&bytes);
281        // Schedule the hash for the next save without forcing a save here, so the
282        // checkpoint write coalesces with the rest of the block's batch.
283        self.force_stored_hash = Some(hash);
284        *self.hash.get_mut().unwrap() = None;
285        Ok((bytes, hash))
286    }
287
288    /// Replaces the inner view's persisted content with `bytes` (a prior `dump_content`
289    /// output) and atomically records the hash of those bytes as the new stored hash.
290    /// Returns the recorded hash, so the caller can compare against an expected value.
291    ///
292    /// The replacement is save-atomic: this method writes a single batch containing the
293    /// inner-prefix wipe, the decoded `(key, value)` puts, and the wrapper's hash key.
294    /// The wrapper's normal `pre_save` lifecycle is bypassed.
295    ///
296    /// **The inner view's in-memory state is undefined after this returns.** Callers
297    /// should drop the view and reload before using it further.
298    pub async fn restore_from_content(&mut self, bytes: &[u8]) -> Result<HasherOutput, ViewError> {
299        let entries = decode_key_values(bytes)?;
300        let hash = hash_bytes(bytes);
301
302        let context = self.inner.context();
303        let inner_base = context.base_key().bytes.clone();
304        let mut wrapper_hash_key = inner_base.clone();
305        // The inner context's base key is `<wrapper_base><Inner tag>`; flipping the last
306        // byte to `Hash` gives the wrapper's hash key.
307        *wrapper_hash_key
308            .last_mut()
309            .expect("inner base key is non-empty") = KeyTag::Hash as u8;
310
311        let mut batch = Batch::new();
312        // Wipe whatever is currently under the inner prefix.
313        batch.delete_key_prefix(inner_base.clone());
314        // Re-insert the decoded entries.
315        for (key, value) in entries {
316            let mut full_key = inner_base.clone();
317            full_key.extend_from_slice(&key);
318            batch.put_key_value_bytes(full_key, value);
319        }
320        // Persist the new stored hash atomically with the content replacement.
321        batch.put_key_value(wrapper_hash_key, &hash)?;
322
323        context
324            .store()
325            .write_batch(batch)
326            .await
327            .map_err(|err| ViewError::StoreError {
328                backend: "HistoricallyHashableView::restore_from_content",
329                error: Box::new(err),
330                must_reload_view: false,
331            })?;
332
333        // Update wrapper in-memory state; the inner view's in-memory state is now stale
334        // and the caller is contractually obliged to reload.
335        self.stored_hash = Some(hash);
336        *self.hash.get_mut().unwrap() = Some(hash);
337        self.force_stored_hash = None;
338
339        Ok(hash)
340    }
341}
342
343/// Decodes a canonical content byte string (a BCS-encoded `Vec<(Vec<u8>, Vec<u8>)>`)
344/// and validates that keys are in strictly increasing lexicographic order. Returns
345/// [`ViewError::MalformedContent`] if the ordering invariant is violated; BCS framing
346/// errors surface as [`ViewError::BcsError`].
347///
348/// The order check is at this layer rather than relying on BCS, because BCS does not
349/// constrain element ordering — only the bytes representation given a value. Two
350/// callers building entries in different orders would produce different bytes and
351/// different hashes; canonical content must always be sorted.
352#[expect(clippy::type_complexity)]
353fn decode_key_values(bytes: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>, ViewError> {
354    let entries: Vec<(Vec<u8>, Vec<u8>)> = bcs::from_bytes(bytes)?;
355    for window in entries.windows(2) {
356        if window[1].0 <= window[0].0 {
357            return Err(ViewError::MalformedContent(
358                "keys must be in strictly increasing order",
359            ));
360        }
361    }
362    Ok(entries)
363}
364
365fn hash_bytes(bytes: &[u8]) -> HasherOutput {
366    // Domain-separation tag: ensures the SHA3 input here cannot equal the SHA3 input of
367    // `make_hash` (`<32-byte stored_hash> || bcs(batch)`). For the two SHA3 inputs to
368    // coincide, an attacker would need a stored_hash equal to the first 32 bytes of this
369    // tag — i.e., a SHA3-256 preimage attack. The tag is therefore at least 32 bytes long.
370    const DOMAIN_TAG: &[u8] = b"linera-views::HistoricallyHashableView::dump_content/v1";
371    const _: () = assert!(DOMAIN_TAG.len() >= 32);
372    let mut hasher = sha3::Sha3_256::default();
373    hasher
374        .update_with_bytes(DOMAIN_TAG)
375        .expect("Sha3_256 hashing of a byte slice cannot fail");
376    hasher
377        .update_with_bytes(bytes)
378        .expect("Sha3_256 hashing of a byte slice cannot fail");
379    hasher.finalize()
380}
381
382impl<C, W> Deref for HistoricallyHashableView<C, W> {
383    type Target = W;
384
385    fn deref(&self) -> &W {
386        &self.inner
387    }
388}
389
390impl<C, W> DerefMut for HistoricallyHashableView<C, W> {
391    fn deref_mut(&mut self) -> &mut W {
392        // Clear the memoized hash.
393        *self.hash.get_mut().unwrap() = None;
394        &mut self.inner
395    }
396}
397
398#[cfg(with_graphql)]
399mod graphql {
400    use std::borrow::Cow;
401
402    use super::HistoricallyHashableView;
403    use crate::context::Context;
404
405    impl<C, W> async_graphql::OutputType for HistoricallyHashableView<C, W>
406    where
407        C: Context,
408        W: async_graphql::OutputType + Send + Sync,
409    {
410        fn type_name() -> Cow<'static, str> {
411            W::type_name()
412        }
413
414        fn qualified_type_name() -> String {
415            W::qualified_type_name()
416        }
417
418        fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String {
419            W::create_type_info(registry)
420        }
421
422        async fn resolve(
423            &self,
424            ctx: &async_graphql::ContextSelectionSet<'_>,
425            field: &async_graphql::Positioned<async_graphql::parser::types::Field>,
426        ) -> async_graphql::ServerResult<async_graphql::Value> {
427            self.inner.resolve(ctx, field).await
428        }
429    }
430}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::{context::MemoryContext, register_view::RegisterView};
436
437    #[tokio::test]
438    async fn test_historically_hashable_view_initial_state() -> Result<(), ViewError> {
439        let context = MemoryContext::new_for_testing(());
440        let mut view =
441            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
442
443        // Initially should have no pending changes
444        assert!(!view.has_pending_changes().await);
445
446        // Initial hash should be the hash of an empty batch with default stored_hash
447        let hash = view.historical_hash().await?;
448        assert_eq!(hash, HasherOutput::default());
449
450        Ok(())
451    }
452
453    #[tokio::test]
454    async fn test_historically_hashable_view_hash_changes_with_modifications(
455    ) -> Result<(), ViewError> {
456        let context = MemoryContext::new_for_testing(());
457        let mut view =
458            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
459
460        // Get initial hash
461        let hash0 = view.historical_hash().await?;
462
463        // Set a value
464        view.set(42);
465        assert!(view.has_pending_changes().await);
466
467        // Hash should change after modification
468        let hash1 = view.historical_hash().await?;
469
470        // Calling `historical_hash` doesn't flush changes.
471        assert!(view.has_pending_changes().await);
472        assert_ne!(hash0, hash1);
473
474        // Flush and verify hash is stored
475        let mut batch = Batch::new();
476        view.pre_save(&mut batch)?;
477        context.store().write_batch(batch).await?;
478        view.post_save();
479        assert!(!view.has_pending_changes().await);
480        assert_eq!(hash1, view.historical_hash().await?);
481
482        // Make another modification
483        view.set(84);
484        let hash2 = view.historical_hash().await?;
485        assert_ne!(hash1, hash2);
486
487        Ok(())
488    }
489
490    #[tokio::test]
491    async fn test_historically_hashable_view_reloaded() -> Result<(), ViewError> {
492        let context = MemoryContext::new_for_testing(());
493        let mut view =
494            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
495
496        // Set initial value and flush
497        view.set(42);
498        let mut batch = Batch::new();
499        view.pre_save(&mut batch)?;
500        context.store().write_batch(batch).await?;
501        view.post_save();
502
503        let hash_after_flush = view.historical_hash().await?;
504
505        // Reload the view
506        let mut view2 =
507            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
508
509        // Hash should be the same (loaded from storage)
510        let hash_reloaded = view2.historical_hash().await?;
511        assert_eq!(hash_after_flush, hash_reloaded);
512
513        Ok(())
514    }
515
516    #[tokio::test]
517    async fn test_historically_hashable_view_rollback() -> Result<(), ViewError> {
518        let context = MemoryContext::new_for_testing(());
519        let mut view =
520            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
521
522        // Set and persist a value
523        view.set(42);
524        let mut batch = Batch::new();
525        view.pre_save(&mut batch)?;
526        context.store().write_batch(batch).await?;
527        view.post_save();
528
529        let hash_before = view.historical_hash().await?;
530        assert!(!view.has_pending_changes().await);
531
532        // Make a modification
533        view.set(84);
534        assert!(view.has_pending_changes().await);
535        let hash_modified = view.historical_hash().await?;
536        assert_ne!(hash_before, hash_modified);
537
538        // Rollback
539        view.rollback();
540        assert!(!view.has_pending_changes().await);
541
542        // Hash should return to previous value
543        let hash_after_rollback = view.historical_hash().await?;
544        assert_eq!(hash_before, hash_after_rollback);
545
546        Ok(())
547    }
548
549    #[tokio::test]
550    async fn test_historically_hashable_view_clear() -> Result<(), ViewError> {
551        let context = MemoryContext::new_for_testing(());
552        let mut view =
553            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
554
555        // Set and persist a value
556        view.set(42);
557        let mut batch = Batch::new();
558        view.pre_save(&mut batch)?;
559        context.store().write_batch(batch).await?;
560        view.post_save();
561
562        assert_ne!(view.historical_hash().await?, HasherOutput::default());
563
564        // Clear the view
565        view.clear();
566        assert!(view.has_pending_changes().await);
567
568        // Flush the clear operation
569        let mut batch = Batch::new();
570        let delete_view = view.pre_save(&mut batch)?;
571        assert!(!delete_view);
572        context.store().write_batch(batch).await?;
573        view.post_save();
574
575        // Verify the view is not reset to default
576        assert_ne!(view.historical_hash().await?, HasherOutput::default());
577
578        Ok(())
579    }
580
581    #[tokio::test]
582    async fn test_historically_hashable_view_clone_unchecked() -> Result<(), ViewError> {
583        let context = MemoryContext::new_for_testing(());
584        let mut view =
585            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
586
587        // Set a value
588        view.set(42);
589        let mut batch = Batch::new();
590        view.pre_save(&mut batch)?;
591        context.store().write_batch(batch).await?;
592        view.post_save();
593
594        let original_hash = view.historical_hash().await?;
595
596        // Clone the view
597        let mut cloned_view = view.clone_unchecked()?;
598
599        // Verify the clone has the same hash initially
600        let cloned_hash = cloned_view.historical_hash().await?;
601        assert_eq!(original_hash, cloned_hash);
602
603        // Modify the clone
604        cloned_view.set(84);
605        let cloned_hash_after = cloned_view.historical_hash().await?;
606        assert_ne!(original_hash, cloned_hash_after);
607
608        // Original should be unchanged
609        let original_hash_after = view.historical_hash().await?;
610        assert_eq!(original_hash, original_hash_after);
611
612        Ok(())
613    }
614
615    #[tokio::test]
616    async fn test_historically_hashable_view_flush_updates_stored_hash() -> Result<(), ViewError> {
617        let context = MemoryContext::new_for_testing(());
618        let mut view =
619            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
620
621        // Initial state - no stored hash
622        assert!(!view.has_pending_changes().await);
623
624        // Set a value
625        view.set(42);
626        assert!(view.has_pending_changes().await);
627
628        let hash_before_flush = view.historical_hash().await?;
629
630        // Flush - this should update stored_hash
631        let mut batch = Batch::new();
632        let delete_view = view.pre_save(&mut batch)?;
633        assert!(!delete_view);
634        context.store().write_batch(batch).await?;
635        view.post_save();
636
637        assert!(!view.has_pending_changes().await);
638
639        // Make another change
640        view.set(84);
641        let hash_after_second_change = view.historical_hash().await?;
642
643        // The new hash should be based on the previous stored hash
644        assert_ne!(hash_before_flush, hash_after_second_change);
645
646        Ok(())
647    }
648
649    #[tokio::test]
650    async fn test_historically_hashable_view_deref() -> Result<(), ViewError> {
651        let context = MemoryContext::new_for_testing(());
652        let mut view =
653            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
654
655        // Test Deref - we can access inner view methods directly
656        view.set(42);
657        assert_eq!(*view.get(), 42);
658
659        // Test DerefMut
660        view.set(84);
661        assert_eq!(*view.get(), 84);
662
663        Ok(())
664    }
665
666    #[tokio::test]
667    async fn test_historically_hashable_view_sequential_modifications() -> Result<(), ViewError> {
668        async fn get_hash(values: &[u32]) -> Result<HasherOutput, ViewError> {
669            let context = MemoryContext::new_for_testing(());
670            let mut view =
671                HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
672
673            let mut previous_hash = view.historical_hash().await?;
674            for &value in values {
675                view.set(value);
676                if value % 2 == 0 {
677                    // Immediately save after odd values.
678                    let mut batch = Batch::new();
679                    view.pre_save(&mut batch)?;
680                    context.store().write_batch(batch).await?;
681                    view.post_save();
682                }
683                let current_hash = view.historical_hash().await?;
684                assert_ne!(previous_hash, current_hash);
685                previous_hash = current_hash;
686            }
687            Ok(previous_hash)
688        }
689
690        let h1 = get_hash(&[10, 20, 30, 40, 50]).await?;
691        let h2 = get_hash(&[20, 30, 40, 50]).await?;
692        let h3 = get_hash(&[20, 21, 30, 40, 50]).await?;
693        assert_ne!(h1, h2);
694        assert_eq!(h2, h3);
695        Ok(())
696    }
697
698    #[tokio::test]
699    async fn test_historically_hashable_view_flush_with_no_hash_change() -> Result<(), ViewError> {
700        let context = MemoryContext::new_for_testing(());
701        let mut view =
702            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
703
704        // Set and flush a value
705        view.set(42);
706        let mut batch = Batch::new();
707        view.pre_save(&mut batch)?;
708        context.store().write_batch(batch).await?;
709        view.post_save();
710
711        let hash_before = view.historical_hash().await?;
712
713        // Flush again without changes - no new hash should be stored
714        let mut batch = Batch::new();
715        view.pre_save(&mut batch)?;
716        assert!(batch.is_empty());
717        context.store().write_batch(batch).await?;
718        view.post_save();
719
720        let hash_after = view.historical_hash().await?;
721        assert_eq!(hash_before, hash_after);
722
723        Ok(())
724    }
725
726    #[tokio::test]
727    async fn test_dump_content_then_save_records_content_hash() -> Result<(), ViewError> {
728        // Persist some inner state, dump it, then save. The on-disk stored hash should be
729        // the hash of the canonical bytes — independent of the prior history-of-batches
730        // hash that the view had before dumping.
731        let context = MemoryContext::new_for_testing(());
732        let mut view =
733            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
734
735        view.set(42);
736        let mut batch = Batch::new();
737        view.pre_save(&mut batch)?;
738        context.store().write_batch(batch).await?;
739        view.post_save();
740
741        let history_hash_before = view.historical_hash().await?;
742
743        let (bytes, content_hash) = view.dump_content().await?;
744        assert_ne!(history_hash_before, content_hash);
745        assert_eq!(view.historical_hash().await?, content_hash);
746
747        // Save: the override hash is persisted.
748        let mut batch = Batch::new();
749        view.pre_save(&mut batch)?;
750        context.store().write_batch(batch).await?;
751        view.post_save();
752
753        // Reload to confirm `stored_hash` on disk is now the content hash.
754        let mut reloaded =
755            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
756        assert_eq!(reloaded.historical_hash().await?, content_hash);
757        assert_eq!(*reloaded.get(), 42);
758
759        // And re-dumping produces the same bytes (canonical layout is deterministic).
760        let (bytes_again, content_hash_again) = reloaded.dump_content().await?;
761        assert_eq!(bytes, bytes_again);
762        assert_eq!(content_hash, content_hash_again);
763
764        Ok(())
765    }
766
767    #[tokio::test]
768    async fn test_restore_then_reload_matches_source() -> Result<(), ViewError> {
769        // Dump from one view's state, restore those bytes into a fresh store, reload, and
770        // confirm the restored view reports the same content hash and the same value.
771        let source_context = MemoryContext::new_for_testing(());
772        let mut source =
773            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(source_context.clone())
774                .await?;
775        source.set(7);
776        let mut batch = Batch::new();
777        source.pre_save(&mut batch)?;
778        source_context.store().write_batch(batch).await?;
779        source.post_save();
780        let (bytes, expected_hash) = source.dump_content().await?;
781
782        // Restore into a fresh context and reload.
783        let target_context = MemoryContext::new_for_testing(());
784        let mut target =
785            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(target_context.clone())
786                .await?;
787        let restored_hash = target.restore_from_content(&bytes).await?;
788        assert_eq!(restored_hash, expected_hash);
789
790        // Caller is contractually obliged to reload after restore.
791        let mut reloaded =
792            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(target_context.clone())
793                .await?;
794        assert_eq!(reloaded.historical_hash().await?, expected_hash);
795        assert_eq!(*reloaded.get(), 7);
796
797        // And re-dumping produces identical bytes — full round-trip.
798        let (bytes_after_restore, _) = reloaded.dump_content().await?;
799        assert_eq!(bytes, bytes_after_restore);
800
801        Ok(())
802    }
803
804    #[tokio::test]
805    async fn test_dump_content_errors_on_pending_changes() -> Result<(), ViewError> {
806        let context = MemoryContext::new_for_testing(());
807        let mut view =
808            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
809        view.set(1);
810        // The set() call did not flush, so the view has pending changes.
811        match view.dump_content().await {
812            Err(ViewError::HasPendingChanges) => Ok(()),
813            other => panic!("expected HasPendingChanges, got {other:?}"),
814        }
815    }
816
817    #[tokio::test]
818    async fn test_dump_content_rollback_discards_override() -> Result<(), ViewError> {
819        // After dump_content, the view holds a forced-hash override. A rollback should
820        // discard it, restoring the prior history-of-batches hash.
821        let context = MemoryContext::new_for_testing(());
822        let mut view =
823            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
824        view.set(99);
825        let mut batch = Batch::new();
826        view.pre_save(&mut batch)?;
827        context.store().write_batch(batch).await?;
828        view.post_save();
829
830        let history_hash = view.historical_hash().await?;
831        let (_, content_hash) = view.dump_content().await?;
832        assert_eq!(view.historical_hash().await?, content_hash);
833
834        view.rollback();
835        assert_eq!(view.historical_hash().await?, history_hash);
836
837        Ok(())
838    }
839
840    #[tokio::test]
841    async fn test_decode_rejects_unsorted_keys() -> Result<(), ViewError> {
842        // BCS-encode entries in non-increasing key order; restore should reject them.
843        let entries: Vec<(Vec<u8>, Vec<u8>)> = vec![
844            (b"b".to_vec(), b"v1".to_vec()),
845            (b"a".to_vec(), b"v2".to_vec()),
846        ];
847        let bytes = bcs::to_bytes(&entries).expect("encoding cannot fail");
848        let context = MemoryContext::new_for_testing(());
849        let mut view =
850            HistoricallyHashableView::<_, RegisterView<_, u32>>::load(context.clone()).await?;
851        match view.restore_from_content(&bytes).await {
852            Err(ViewError::MalformedContent(_)) => Ok(()),
853            other => panic!("expected MalformedContent, got {other:?}"),
854        }
855    }
856}