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