Barretenberg
The ZK-SNARK library at the core of Aztec
Loading...
Searching...
No Matches
content_addressed_cache.hpp
Go to the documentation of this file.
1// === AUDIT STATUS ===
2// internal: { status: not started, auditors: [], date: YYYY-MM-DD }
3// external_1: { status: not started, auditors: [], date: YYYY-MM-DD }
4// external_2: { status: not started, auditors: [], date: YYYY-MM-DD }
5// =====================
6
7#pragma once
8#include "./tree_meta.hpp"
16#include "msgpack/assert.hpp"
17#include <cstdint>
18#include <exception>
19#include <iostream>
20#include <memory>
21#include <mutex>
22#include <optional>
23#include <sstream>
24#include <stdexcept>
25#include <unordered_map>
26#include <utility>
27#include <vector>
28
30
31// Stores all of the penidng updates to a mekle tree indexed for optimal retrieval
32// Also stores a journal of inverse changes to the cache, enabling checkpoints and
33// and subsequent commit/revert operations
34template <typename LeafValueType> class ContentAddressedCache {
35 public:
40
42 ContentAddressedCache(uint32_t depth);
46 ContentAddressedCache(ContentAddressedCache&& other) noexcept = default;
48 bool operator==(const ContentAddressedCache& other) const = default;
49
50 void checkpoint();
51 void revert();
52 void commit();
53 void revert_all();
54 void commit_all();
55
56 void reset(uint32_t depth);
58 const uint256_t& retrieved_value,
59 const index_t& db_index) const;
60
61 bool get_leaf_preimage_by_hash(const fr& leaf_hash, IndexedLeafValueType& leaf_pre_image) const;
62 void put_leaf_preimage_by_hash(const fr& leaf_hash, const IndexedLeafValueType& leaf_pre_image);
63
64 bool get_leaf_by_index(const index_t& index, IndexedLeafValueType& leaf_pre_image) const;
65 void put_leaf_by_index(const index_t& index, const IndexedLeafValueType& leaf_pre_image);
66
67 void update_leaf_key_index(const index_t& index, const fr& leaf_key);
68 std::optional<index_t> get_leaf_key_index(const fr& leaf_key) const;
69
70 void put_node(const fr& node_hash, const NodePayload& node);
71 bool get_node(const fr& node_hash, NodePayload& node) const;
72
73 void put_meta(const TreeMeta& meta) { meta_ = meta; }
74 const TreeMeta& get_meta() const { return meta_; }
75
76 std::optional<fr> get_node_by_index(uint32_t level, const index_t& index) const;
77 void put_node_by_index(uint32_t level, const index_t& index, const fr& node);
78
80
81 bool is_equivalent_to(const ContentAddressedCache& other) const;
82
83 private:
84 struct Journal {
85 // Captures the tree's metadata at the time of checkpoint
87 // Captures the cache's node hashes at the time of checkpoint. If the node does not exist in the cache, the
88 // optional will == nullopt
89 // TODO (PhilWindle): Consider where a more optimal approach is a single unordered map, instead of 1 per level
91 // Captures the cache's leaf pre-images at the time of checkpoint. Again, if the leaf does not exist in the
92 // cache, the optional will == nullopt
94 // Captures the addition of new leaf keys into the indices_ cache
96
98 : meta_(std::move(meta))
99 , nodes_by_index_(meta_.depth + 1, std::unordered_map<index_t, std::optional<fr>>())
100 {}
101 };
102 // This is a mapping between the node hash and it's payload (children and ref count) for every node in the tree,
103 // including leaves. As indexed trees are updated, this will end up containing many nodes that are not part of the
104 // final tree so they need to be omitted from what is committed.
106
107 // This is a store mapping the leaf key (e.g. slot for public data or nullifier value for nullifier tree) to the
108 // index in the tree
110
111 // This is a mapping from leaf hash to leaf pre-image. This will contain entries that need to be omitted when
112 // commiting updates
115
116 // The following stores are not persisted, just cached until commit
119
120 // The currently active journals
122};
123
124template <typename LeafValueType> ContentAddressedCache<LeafValueType>::ContentAddressedCache(uint32_t depth)
125{
126 reset(depth);
127}
128
129template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::checkpoint()
130{
131 journals_.emplace_back(Journal(meta_));
132}
133
134template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::revert()
135{
136 if (journals_.empty()) {
137 throw std::runtime_error("Cannot revert without a checkpoint");
138 }
139 // We need to iterate over the nodes and leaves and
140 // 1. Remove any that were added since last checkpoint
141 // 2. Restore any that were updated since last checkpoint
142 // 3. Remove any new leaf keys that were added to the indices store
143 // 4. Restore the meta data
144
145 Journal& journal = journals_.back();
146
147 for (uint32_t i = 0; i < journal.nodes_by_index_.size(); ++i) {
148 for (const auto& [index, optional_node_hash] : journal.nodes_by_index_[i]) {
149 // If the optional == nullopt then we remove it from the primary cache, it never existed before
150 if (!optional_node_hash.has_value()) {
151 nodes_by_index_[i].erase(index);
152 } else {
153 // The optional is not null, this means there is a vlue to be restored to the primary cache
154 nodes_by_index_[i][index] = optional_node_hash.value();
155 }
156 }
157 }
158
159 for (const auto& [index, optional_leaf] : journal.leaf_pre_image_by_index_) {
160 // If the option == nullopt then we remove it from the primary cache, it never existed before
161 // Also remove from the indices store
162 if (!optional_leaf.has_value()) {
163 leaf_pre_image_by_index_.erase(index);
164 } else {
165 // There was a leaf pre-image, restore it to the primary cache
166 // No need to update the indices store as the key has not changed
167 leaf_pre_image_by_index_[index] = optional_leaf.value();
168 }
169 }
170
171 // Remove any newly added leaf keys
172 for (const auto& key : journal.new_leaf_keys_) {
173 indices_.erase(key);
174 }
175
176 // We need to restore the meta data
177 meta_ = std::move(journal.meta_);
178 journals_.pop_back();
179}
180
181template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::commit()
182{
183 if (journals_.empty()) {
184 throw std::runtime_error("Cannot commit without a checkpoint");
185 }
186
187 // We need to iterate over the nodes and leaves and merge them into the previous checkpoint if there is one
188 // We also need to append any newly added leaf keys to the previous checkpoint
189 // If there is no previous checkpoint then we just destroy the journal as the cache will be correct
190
191 if (journals_.size() == 1) {
192 journals_.clear();
193 return;
194 }
195
196 Journal& current_journal = journals_.back();
197 Journal& previous_journal = journals_[journals_.size() - 2];
198
199 for (uint32_t i = 0; i < current_journal.nodes_by_index_.size(); ++i) {
200 for (const auto& [index, optional_node_hash] : current_journal.nodes_by_index_[i]) {
201 // There is an entry in the current journal, if it does not exist in the previous journal then we need to
202 // add it If it does exist in the previous journal then that journal already captured a value from the
203 // primary cache that existed no later
204 auto previousIter = previous_journal.nodes_by_index_[i].find(index);
205 if (previousIter == previous_journal.nodes_by_index_[i].end()) {
206 previous_journal.nodes_by_index_[i][index] = optional_node_hash;
207 }
208 }
209 }
210
211 for (const auto& [index, optional_leaf] : current_journal.leaf_pre_image_by_index_) {
212 // There is an entry in the current journal, if it does not exist in the previous journal then we need to add it
213 // If it does exist in the previous journal then that journal already captured a value from the
214 // primary cache that existed no later
215 auto previousIter = previous_journal.leaf_pre_image_by_index_.find(index);
216 if (previousIter == previous_journal.leaf_pre_image_by_index_.end()) {
217 previous_journal.leaf_pre_image_by_index_[index] = optional_leaf;
218 }
219 }
220
221 // Add our newly appended leaf keys to those of the previous journal
222 previous_journal.new_leaf_keys_.insert(previous_journal.new_leaf_keys_.end(),
223 current_journal.new_leaf_keys_.cbegin(),
224 current_journal.new_leaf_keys_.cend());
225
226 // We don't restore the meta here. We are committing, so the primary cached meta is correct
227 journals_.pop_back();
228}
229
230template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::commit_all()
231{
232 while (!journals_.empty()) {
233 commit();
234 }
235}
236
237template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::revert_all()
238{
239 while (!journals_.empty()) {
240 revert();
241 }
242}
243template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::reset(uint32_t depth)
244{
246 indices_ = std::map<uint256_t, index_t>();
249 leaf_pre_image_by_index_ = std::unordered_map<index_t, IndexedLeafValueType>();
250 journals_ = std::vector<Journal>();
251}
252
253template <typename LeafValueType>
255{
256 // Meta should be identical
257 if (meta_ != other.meta_) {
258 return false;
259 }
260
261 // Indices should be identical
262 if (indices_ != other.indices_) {
263 return false;
264 }
265
266 // Nodes by index should be identical
267 if (nodes_by_index_ != other.nodes_by_index_) {
268 return false;
269 }
270
271 // Leaf pre-images by index should be identical
272 if (leaf_pre_image_by_index_ != other.leaf_pre_image_by_index_) {
273 return false;
274 }
275
276 // Our leaves should be a subset of the other leaves
277 for (const auto& [leaf_hash, leaf] : leaves_) {
278 auto it = other.leaves_.find(leaf_hash);
279 if (it == other.leaves_.end()) {
280 return false;
281 }
282 if (it->second != leaf) {
283 return false;
284 }
285 }
286
287 // Our nodes should be a subset of the other nodes
288 for (const auto& [node_hash, node] : nodes_) {
289 auto it = other.nodes_.find(node_hash);
290 if (it == other.nodes_.end()) {
291 return false;
292 }
293 if (it->second != node) {
294 return false;
295 }
296 }
297 return true;
298}
299
300template <typename LeafValueType>
302 const uint256_t& retrieved_value,
303 const index_t& db_index) const
304{
305 if (indices_.empty()) {
306 return std::make_pair(new_leaf_key == retrieved_value, db_index);
307 }
308 // At this stage, we have been asked to include uncommitted and the value was not exactly found in the db
309 auto it = indices_.lower_bound(new_leaf_key);
310 if (it == indices_.end()) {
311 // there is no element >= the requested value.
312 // decrement the iterator to get the value preceeding the requested value
313 --it;
314 // we need to return the larger of the db value or the cached value
315
316 return std::make_pair(false, it->first > retrieved_value ? it->second : db_index);
317 }
318
319 if (it->first == new_leaf_key) {
320 // the value is already present and the iterator points to it
321 return std::make_pair(true, it->second);
322 }
323 // the iterator points to the element immediately larger than the requested value
324 // We need to return the highest value from
325 // 1. The next lowest cached value, if there is one
326 // 2. The value retrieved from the db
327 if (it == indices_.begin()) {
328 // No cached lower value, return the db index
329 return std::make_pair(false, db_index);
330 }
331 --it;
332 // it now points to the value less than that requested
333 return std::make_pair(false, it->first > retrieved_value ? it->second : db_index);
334}
335
336template <typename LeafValueType>
338 IndexedLeafValueType& leaf_pre_image) const
339{
340 typename std::unordered_map<fr, IndexedLeafValueType>::const_iterator it = leaves_.find(leaf_hash);
341 if (it != leaves_.end()) {
342 leaf_pre_image = it->second;
343 return true;
344 }
345 return false;
346}
347
348template <typename LeafValueType>
350 const IndexedLeafValueType& leaf_pre_image)
351{
352 leaves_[leaf_hash] = leaf_pre_image;
353}
354
355template <typename LeafValueType>
357 IndexedLeafValueType& leaf_pre_image) const
358{
359 typename std::unordered_map<index_t, IndexedLeafValueType>::const_iterator it =
360 leaf_pre_image_by_index_.find(index);
361 if (it != leaf_pre_image_by_index_.end()) {
362 leaf_pre_image = it->second;
363 return true;
364 }
365 return false;
366}
367
368template <typename LeafValueType>
370 const IndexedLeafValueType& leaf_pre_image)
371{
372 // If there is no current journal then we just update the cache and leave
373 if (journals_.empty()) {
374 leaf_pre_image_by_index_[index] = leaf_pre_image;
375 return;
376 }
377
378 // There is a journal, grab it
379 Journal& journal = journals_.back();
380
381 // If there is no leaf pre-image at the given index then add the index location to the journal's collection of empty
382 // locations
383 auto cache_iter = leaf_pre_image_by_index_.find(index);
384 if (cache_iter == leaf_pre_image_by_index_.end()) {
385 journal.leaf_pre_image_by_index_[index] = std::nullopt;
386 } else {
387 // There is a leaf pre-image. If the journal does not have a pre-image at this index then add it to the journal
388 auto journalIter = journal.leaf_pre_image_by_index_.find(index);
389 if (journalIter == journal.leaf_pre_image_by_index_.end()) {
390 journal.leaf_pre_image_by_index_[index] = cache_iter->second;
391 }
392 }
393 leaf_pre_image_by_index_[index] = leaf_pre_image;
394}
395
396template <typename LeafValueType>
398{
399 uint256_t key = uint256_t(leaf_key);
400 auto result = indices_.insert({ key, index });
401 if (result.second && !journals_.empty()) {
402 // The insertion took place, if we have a current journal then we need to add to the newly inserted leaf keys
403 Journal& journal = journals_.back();
404 journal.new_leaf_keys_.emplace_back(key);
405 }
406}
407
408template <typename LeafValueType>
410{
411 auto it = indices_.find(uint256_t(leaf_key));
412 if (it == indices_.end()) {
413 return std::nullopt;
414 }
415 return it->second;
416}
417
418template <typename LeafValueType>
420{
421 nodes_[node_hash] = node;
422}
423
424template <typename LeafValueType>
426{
427 auto it = nodes_.find(node_hash);
428 if (it == nodes_.end()) {
429 return false;
430 }
431 node = it->second;
432 return true;
433}
434
435template <typename LeafValueType>
437{
438 auto it = nodes_by_index_[level].find(index);
439 if (it == nodes_by_index_[level].end()) {
440 return std::nullopt;
441 }
442 return it->second;
443}
444
445template <typename LeafValueType>
446void ContentAddressedCache<LeafValueType>::put_node_by_index(uint32_t level, const index_t& index, const fr& node)
447{
448 // If there is no current journal then we just update the cache and leave
449 if (journals_.empty()) {
450 nodes_by_index_[level][index] = node;
451 return;
452 }
453
454 // There is a journal, grab it
455 Journal& journal = journals_.back();
456
457 // If there is no node at the given location then add a nullopt to the journal
458 auto cacheIter = nodes_by_index_[level].find(index);
459 if (cacheIter == nodes_by_index_[level].end()) {
460 journal.nodes_by_index_[level][index] = std::nullopt;
461 } else {
462 // There is a node. If the journal does not have a node at this index then add it to the journal
463 auto journalIter = journal.nodes_by_index_[level].find(index);
464 if (journalIter == journal.nodes_by_index_[level].end()) {
465 journal.nodes_by_index_[level][index] = cacheIter->second;
466 }
467 }
468 nodes_by_index_[level][index] = node;
469}
470} // namespace bb::crypto::merkle_tree
std::unique_ptr< ContentAddressedCache > UniquePtr
void put_leaf_by_index(const index_t &index, const IndexedLeafValueType &leaf_pre_image)
bool get_leaf_by_index(const index_t &index, IndexedLeafValueType &leaf_pre_image) const
ContentAddressedCache & operator=(const ContentAddressedCache &other)=default
void update_leaf_key_index(const index_t &index, const fr &leaf_key)
std::unordered_map< fr, IndexedLeafValueType > leaves_
void put_leaf_preimage_by_hash(const fr &leaf_hash, const IndexedLeafValueType &leaf_pre_image)
std::optional< fr > get_node_by_index(uint32_t level, const index_t &index) const
const std::map< uint256_t, index_t > & get_indices() const
std::shared_ptr< ContentAddressedCache > SharedPtr
std::optional< index_t > get_leaf_key_index(const fr &leaf_key) const
void put_node(const fr &node_hash, const NodePayload &node)
ContentAddressedCache(ContentAddressedCache &&other) noexcept=default
std::pair< bool, index_t > find_low_value(const uint256_t &new_leaf_key, const uint256_t &retrieved_value, const index_t &db_index) const
bool get_node(const fr &node_hash, NodePayload &node) const
bool is_equivalent_to(const ContentAddressedCache &other) const
bool get_leaf_preimage_by_hash(const fr &leaf_hash, IndexedLeafValueType &leaf_pre_image) const
ContentAddressedCache(const ContentAddressedCache &other)=default
bool operator==(const ContentAddressedCache &other) const =default
std::unordered_map< index_t, IndexedLeafValueType > leaf_pre_image_by_index_
void put_node_by_index(uint32_t level, const index_t &index, const fr &node)
std::vector< std::unordered_map< index_t, fr > > nodes_by_index_
ContentAddressedCache & operator=(ContentAddressedCache &&other) noexcept=default
PublicDataLeafValue LeafValueType
STL namespace.
constexpr decltype(auto) get(::tuplet::tuple< T... > &&t) noexcept
Definition tuple.hpp:13
std::vector< std::unordered_map< index_t, std::optional< fr > > > nodes_by_index_
std::unordered_map< index_t, std::optional< IndexedLeafValueType > > leaf_pre_image_by_index_