jmt_blake3/overlay.rs
1use std::collections::HashMap;
2
3use anyhow::Result;
4use tracing::instrument;
5
6use crate::{
7 storage::{TreeReader, TreeWriter},
8 JellyfishMerkleTree, KeyHash, MissingRootError, OwnedValue, RootHash, Version,
9};
10
11// There's some similarity between the OverlayTree and the TreeCache, but it's
12// not exactly clear how to share code between them (the TreeCache operates at
13// the level of the tree internals, not at the level of its external interface),
14// so it's easier and better for now to just make a new wrapper.
15
16/// A wrapper around a [`JellyfishMerkleTree`] that buffers pending writes to the
17/// tree, and overlays the effect of those writes on the tree state for reading.
18pub struct WriteOverlay<R> {
19 reader: R,
20 overlay: HashMap<KeyHash, OwnedValue>,
21 version: Version,
22}
23
24impl<R> WriteOverlay<R> {
25 /// Use this [`Version`] with [`Self::new`] to specify that the writes
26 /// should be committed with version `0`.
27 pub const PRE_GENESIS_VERSION: Version = u64::MAX;
28}
29
30impl<R> WriteOverlay<R>
31where
32 R: TreeReader + Sync,
33{
34 /// Constructs a new [`WriteOverlay`] with the given `reader` and `version`.
35 ///
36 /// All reads performed with `get` will use `version` when querying the
37 /// underlying backing store. The buffered writes created with `put` will
38 /// be written as `version + 1`, so `version` should probably be the latest
39 /// version if `commit` will be called.
40 ///
41 /// To initialize an empty tree, use [`Self::PRE_GENESIS_VERSION`] here.
42 pub fn new(reader: R, version: Version) -> Self {
43 Self {
44 reader,
45 version,
46 overlay: Default::default(),
47 }
48 }
49
50 fn tree(&self) -> JellyfishMerkleTree<'_, R> {
51 JellyfishMerkleTree::new(&self.reader)
52 }
53
54 /// Gets a value by key.
55 ///
56 /// This method reflects the results of any pending writes made by `put`.
57 #[instrument(name = "WriteOverlay::get", skip(self, key))]
58 pub async fn get(&self, key: KeyHash) -> Result<Option<OwnedValue>> {
59 if let Some(value) = self.overlay.get(&key) {
60 tracing::trace!(?key, value = ?hex::encode(&value), "read from cache");
61 Ok(Some(value.clone()))
62 } else {
63 match self.tree().get(key, self.version).await {
64 Ok(Some(value)) => {
65 tracing::trace!(version = ?self.version, ?key, value = ?hex::encode(&value), "read from tree");
66 Ok(Some(value))
67 }
68 Ok(None) => {
69 tracing::trace!(version = ?self.version, ?key, "key not found in tree");
70 Ok(None)
71 }
72 // This allows for using the Overlay on an empty database without errors
73 Err(e) if e.downcast_ref::<MissingRootError>().is_some() => {
74 tracing::trace!(version = ?self.version, "no data available at this version");
75 Ok(None)
76 }
77 Err(e) => Err(e),
78 }
79 }
80 }
81
82 /// Gets a value by key alongside an ICS23 existence proof of that value.
83 ///
84 /// This method does *not* reflect results of any pending writes to the WriteOverlay. An error
85 /// will be returned if the key exists in the WriteOverlay, or if the key does not exist in the
86 /// tree.
87 #[instrument(name = "WriteOverlay::get_with_proof", skip(self, key))]
88 pub async fn get_with_proof(
89 &self,
90 key: Vec<u8>,
91 ) -> Result<(OwnedValue, ics23_blake3::ExistenceProof)> {
92 if self.overlay.contains_key(&key.clone().into()) {
93 return Err(anyhow::anyhow!("key is not yet committed to tree"));
94 }
95 let proof = self.tree().get_with_ics23_proof(key, self.version).await?;
96
97 Ok((proof.value.clone(), proof))
98 }
99
100 /// Puts a key/value pair in the overlay.
101 ///
102 /// Assuming it is not overwritten by a subsequent `put`, the value will be
103 /// written to the tree when `commit` is called.
104 #[instrument(name = "WriteOverlay::put", skip(self, key, value))]
105 pub fn put(&mut self, key: KeyHash, value: OwnedValue) {
106 tracing::trace!(?key, value = ?hex::encode(&value));
107 *self.overlay.entry(key).or_default() = value;
108 }
109
110 /// Clears the overlay, committing all pending writes to the provided
111 /// `writer` and returning the new [`RootHash`] and [`Version`].
112 ///
113 /// The overlay will then point at the newly written state and tree version.
114 #[instrument(name = "WriteOverlay::commit", skip(self, writer))]
115 pub async fn commit<W>(&mut self, mut writer: W) -> Result<(RootHash, Version)>
116 where
117 W: TreeWriter + Sync,
118 {
119 // Committing an empty write overlay will panic in the underlying JMT, so we provide a more
120 // meaningful panic message here instead.
121 if self.overlay.is_empty() {
122 panic!("cannot commit empty `WriteOverlay`");
123 }
124
125 let overlay = std::mem::take(&mut self.overlay);
126 // We use wrapping_add here so that we can write `new_version = 0` by
127 // overflowing `PRE_GENESIS_VERSION`.
128 let new_version = self.version.wrapping_add(1);
129 tracing::trace!(old_version = ?self.version, new_version, ?overlay);
130 let (root_hash, batch) = self
131 .tree()
132 .put_value_set(overlay.into_iter().collect(), new_version)
133 .await?;
134
135 writer.write_node_batch(&batch.node_batch).await?;
136 tracing::trace!(?root_hash, "wrote node batch to backing store");
137
138 // Now that we've successfully written the new nodes, update the version.
139 self.version = new_version;
140
141 Ok((root_hash, new_version))
142 }
143}