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: Complete, auditors: [Nishat], commit: 22d6fc368da0fbe5412f4f7b2890a052aa48d803 }
3// external_1: { status: not started, auditors: [], commit: }
4// external_2: { status: not started, auditors: [], commit: }
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
46 ContentAddressedCache(ContentAddressedCache&& other) noexcept = default;
48 bool operator==(const ContentAddressedCache& other) const = default;
49
50 uint32_t checkpoint();
51 void revert();
52 void commit();
53 void revert_all();
54 void commit_all();
55 void commit_to_depth(uint32_t depth);
56 void revert_to_depth(uint32_t depth);
57 uint32_t depth() const;
58
59 void reset(uint32_t depth);
61 const uint256_t& retrieved_value,
62 const index_t& db_index) const;
63
64 bool get_leaf_preimage_by_hash(const fr& leaf_hash, IndexedLeafValueType& leaf_pre_image) const;
65 void put_leaf_preimage_by_hash(const fr& leaf_hash, const IndexedLeafValueType& leaf_pre_image);
66
67 bool get_leaf_by_index(const index_t& index, IndexedLeafValueType& leaf_pre_image) const;
68 void put_leaf_by_index(const index_t& index, const IndexedLeafValueType& leaf_pre_image);
69
70 void update_leaf_key_index(const index_t& index, const fr& leaf_key);
72
73 void put_node(const fr& node_hash, const NodePayload& node);
74 bool get_node(const fr& node_hash, NodePayload& node) const;
75
76 void put_meta(const TreeMeta& meta) { meta_ = meta; }
77 const TreeMeta& get_meta() const { return meta_; }
78
79 std::optional<fr> get_node_by_index(uint32_t level, const index_t& index) const;
80 void put_node_by_index(uint32_t level, const index_t& index, const fr& node);
81
83
84 bool is_equivalent_to(const ContentAddressedCache& other) const;
85
86 private:
87 struct Journal {
88 // Captures the tree's metadata at the time of checkpoint
90 // Captures the cache's node hashes at the time of checkpoint. If the node does not exist in the cache, the
91 // optional will == nullopt
93 // Captures the cache's leaf pre-images at the time of checkpoint. Again, if the leaf does not exist in the
94 // cache, the optional will == nullopt
96 // Captures the addition of new leaf keys into the indices_ cache
98
100 : meta_(std::move(meta))
101 , nodes_by_index_(meta_.depth + 1, std::unordered_map<index_t, std::optional<fr>>())
102 {}
103 };
104 // This is a mapping between the node hash and it's payload (children and ref count) for every node in the tree,
105 // including leaves. As indexed trees are updated, this will end up containing many nodes that are not part of the
106 // final tree so they need to be omitted from what is committed.
108
109 // This is a store mapping the leaf key (e.g. slot for public data or nullifier value for nullifier tree) to the
110 // index in the tree
112
113 // This is a mapping from leaf hash to leaf pre-image. This will contain entries that need to be omitted when
114 // commiting updates
117
118 // The following stores are not persisted, just cached until commit
121
122 // The currently active journals
124};
125
126template <typename LeafValueType> ContentAddressedCache<LeafValueType>::ContentAddressedCache(uint32_t depth)
127{
128 reset(depth);
129}
130
131template <typename LeafValueType> uint32_t ContentAddressedCache<LeafValueType>::checkpoint()
132{
133 journals_.emplace_back(Journal(meta_));
134 return static_cast<uint32_t>(journals_.size());
135}
136
137template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::revert()
138{
139 if (journals_.empty()) {
140 throw std::runtime_error("Cannot revert without a checkpoint");
141 }
142 // We need to iterate over the nodes and leaves and
143 // 1. Remove any that were added since last checkpoint
144 // 2. Restore any that were updated since last checkpoint
145 // 3. Remove any new leaf keys that were added to the indices store
146 // 4. Restore the meta data
147
148 Journal& journal = journals_.back();
149
150 for (uint32_t i = 0; i < journal.nodes_by_index_.size(); ++i) {
151 for (const auto& [index, optional_node_hash] : journal.nodes_by_index_[i]) {
152 // If the optional == nullopt then we remove it from the primary cache, it never existed before
153 if (!optional_node_hash.has_value()) {
154 nodes_by_index_[i].erase(index);
155 } else {
156 // The optional is not null, this means there is a vlue to be restored to the primary cache
157 nodes_by_index_[i][index] = optional_node_hash.value();
158 }
159 }
160 }
161
162 for (const auto& [index, optional_leaf] : journal.leaf_pre_image_by_index_) {
163 // If the option == nullopt then we remove it from the primary cache, it never existed before
164 // Also remove from the indices store
165 if (!optional_leaf.has_value()) {
166 leaf_pre_image_by_index_.erase(index);
167 } else {
168 // There was a leaf pre-image, restore it to the primary cache
169 // No need to update the indices store as the key has not changed
170 leaf_pre_image_by_index_[index] = optional_leaf.value();
171 }
172 }
173
174 // Remove any newly added leaf keys
175 for (const auto& key : journal.new_leaf_keys_) {
176 indices_.erase(key);
177 }
178
179 // We need to restore the meta data
180 meta_ = std::move(journal.meta_);
181 journals_.pop_back();
182}
183
184template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::commit()
185{
186 if (journals_.empty()) {
187 throw std::runtime_error("Cannot commit without a checkpoint");
188 }
189
190 // We need to iterate over the nodes and leaves and merge them into the previous checkpoint if there is one
191 // We also need to append any newly added leaf keys to the previous checkpoint
192 // If there is no previous checkpoint then we just destroy the journal as the cache will be correct
193
194 if (journals_.size() == 1) {
195 journals_.clear();
196 return;
197 }
198
199 Journal& current_journal = journals_.back();
200 Journal& previous_journal = journals_[journals_.size() - 2];
201
202 for (uint32_t i = 0; i < current_journal.nodes_by_index_.size(); ++i) {
203 for (const auto& [index, optional_node_hash] : current_journal.nodes_by_index_[i]) {
204 // There is an entry in the current journal, if it does not exist in the previous journal then we need to
205 // add it If it does exist in the previous journal then that journal already captured a value from the
206 // primary cache that existed no later
207 auto previousIter = previous_journal.nodes_by_index_[i].find(index);
208 if (previousIter == previous_journal.nodes_by_index_[i].end()) {
209 previous_journal.nodes_by_index_[i][index] = optional_node_hash;
210 }
211 }
212 }
213
214 for (const auto& [index, optional_leaf] : current_journal.leaf_pre_image_by_index_) {
215 // There is an entry in the current journal, if it does not exist in the previous journal then we need to add it
216 // If it does exist in the previous journal then that journal already captured a value from the
217 // primary cache that existed no later
218 auto previousIter = previous_journal.leaf_pre_image_by_index_.find(index);
219 if (previousIter == previous_journal.leaf_pre_image_by_index_.end()) {
220 previous_journal.leaf_pre_image_by_index_[index] = optional_leaf;
221 }
222 }
223
224 // Add our newly appended leaf keys to those of the previous journal
225 previous_journal.new_leaf_keys_.insert(previous_journal.new_leaf_keys_.end(),
226 current_journal.new_leaf_keys_.cbegin(),
227 current_journal.new_leaf_keys_.cend());
228
229 // We don't restore the meta here. We are committing, so the primary cached meta is correct
230 journals_.pop_back();
231}
232
233template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::commit_all()
234{
235 while (!journals_.empty()) {
236 commit();
237 }
238}
239
240template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::revert_all()
241{
242 while (!journals_.empty()) {
243 revert();
244 }
245}
246template <typename LeafValueType> uint32_t ContentAddressedCache<LeafValueType>::depth() const
247{
248 return static_cast<uint32_t>(journals_.size());
249}
250
251template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::commit_to_depth(uint32_t target_depth)
252{
253 if (target_depth >= journals_.size()) {
254 throw std::runtime_error("Invalid depth for commit_to_depth");
255 }
256 while (journals_.size() > target_depth) {
257 commit();
258 }
259}
260
261template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::revert_to_depth(uint32_t target_depth)
262{
263 if (target_depth >= journals_.size()) {
264 throw std::runtime_error("Invalid depth for revert_to_depth");
265 }
266 while (journals_.size() > target_depth) {
267 revert();
268 }
269}
270
271template <typename LeafValueType> void ContentAddressedCache<LeafValueType>::reset(uint32_t depth)
272{
274 indices_ = std::map<uint256_t, index_t>();
277 leaf_pre_image_by_index_ = std::unordered_map<index_t, IndexedLeafValueType>();
278 journals_ = std::vector<Journal>();
279}
280
281template <typename LeafValueType>
283{
284 // Meta should be identical
285 if (meta_ != other.meta_) {
286 return false;
287 }
288
289 // Indices should be identical
290 if (indices_ != other.indices_) {
291 return false;
292 }
293
294 // Nodes by index should be identical
295 if (nodes_by_index_ != other.nodes_by_index_) {
296 return false;
297 }
298
299 // Leaf pre-images by index should be identical
300 if (leaf_pre_image_by_index_ != other.leaf_pre_image_by_index_) {
301 return false;
302 }
303
304 // Our leaves should be a subset of the other leaves
305 for (const auto& [leaf_hash, leaf] : leaves_) {
306 auto it = other.leaves_.find(leaf_hash);
307 if (it == other.leaves_.end()) {
308 return false;
309 }
310 if (it->second != leaf) {
311 return false;
312 }
313 }
314
315 // Our nodes should be a subset of the other nodes
316 for (const auto& [node_hash, node] : nodes_) {
317 auto it = other.nodes_.find(node_hash);
318 if (it == other.nodes_.end()) {
319 return false;
320 }
321 if (it->second != node) {
322 return false;
323 }
324 }
325 return true;
326}
327
328template <typename LeafValueType>
330 const uint256_t& retrieved_value,
331 const index_t& db_index) const
332{
333 if (indices_.empty()) {
334 return std::make_pair(new_leaf_key == retrieved_value, db_index);
335 }
336 // At this stage, we have been asked to include uncommitted and the value was not exactly found in the db
337 auto it = indices_.lower_bound(new_leaf_key);
338 if (it == indices_.end()) {
339 // there is no element >= the requested value.
340 // decrement the iterator to get the value preceeding the requested value
341 --it;
342 // we need to return the larger of the db value or the cached value
343
344 return std::make_pair(false, it->first > retrieved_value ? it->second : db_index);
345 }
346
347 if (it->first == new_leaf_key) {
348 // the value is already present and the iterator points to it
349 return std::make_pair(true, it->second);
350 }
351 // the iterator points to the element immediately larger than the requested value
352 // We need to return the highest value from
353 // 1. The next lowest cached value, if there is one
354 // 2. The value retrieved from the db
355 if (it == indices_.begin()) {
356 // No cached lower value, return the db index
357 return std::make_pair(false, db_index);
358 }
359 --it;
360 // it now points to the value less than that requested
361 return std::make_pair(false, it->first > retrieved_value ? it->second : db_index);
362}
363
364template <typename LeafValueType>
366 IndexedLeafValueType& leaf_pre_image) const
367{
368 typename std::unordered_map<fr, IndexedLeafValueType>::const_iterator it = leaves_.find(leaf_hash);
369 if (it != leaves_.end()) {
370 leaf_pre_image = it->second;
371 return true;
372 }
373 return false;
374}
375
376template <typename LeafValueType>
378 const IndexedLeafValueType& leaf_pre_image)
379{
380 leaves_[leaf_hash] = leaf_pre_image;
381}
382
383template <typename LeafValueType>
385 IndexedLeafValueType& leaf_pre_image) const
386{
387 typename std::unordered_map<index_t, IndexedLeafValueType>::const_iterator it =
388 leaf_pre_image_by_index_.find(index);
389 if (it != leaf_pre_image_by_index_.end()) {
390 leaf_pre_image = it->second;
391 return true;
392 }
393 return false;
394}
395
396template <typename LeafValueType>
398 const IndexedLeafValueType& leaf_pre_image)
399{
400 // If there is no current journal then we just update the cache and leave
401 if (journals_.empty()) {
402 leaf_pre_image_by_index_[index] = leaf_pre_image;
403 return;
404 }
405
406 // There is a journal, grab it
407 Journal& journal = journals_.back();
408
409 // If there is no leaf pre-image at the given index then add the index location to the journal's collection of empty
410 // locations
411 auto cache_iter = leaf_pre_image_by_index_.find(index);
412 if (cache_iter == leaf_pre_image_by_index_.end()) {
414 } else {
415 // There is a leaf pre-image. If the journal does not have a pre-image at this index then add it to the journal
416 auto journalIter = journal.leaf_pre_image_by_index_.find(index);
417 if (journalIter == journal.leaf_pre_image_by_index_.end()) {
418 journal.leaf_pre_image_by_index_[index] = cache_iter->second;
419 }
420 }
421 leaf_pre_image_by_index_[index] = leaf_pre_image;
422}
423
424template <typename LeafValueType>
426{
427 uint256_t key = uint256_t(leaf_key);
428 auto result = indices_.insert({ key, index });
429 if (result.second && !journals_.empty()) {
430 // The insertion took place, if we have a current journal then we need to add to the newly inserted leaf keys
431 Journal& journal = journals_.back();
432 journal.new_leaf_keys_.emplace_back(key);
433 }
434}
435
436template <typename LeafValueType>
438{
439 auto it = indices_.find(uint256_t(leaf_key));
440 if (it == indices_.end()) {
441 return std::nullopt;
442 }
443 return it->second;
444}
445
446template <typename LeafValueType>
448{
449 nodes_[node_hash] = node;
450}
451
452template <typename LeafValueType>
454{
455 auto it = nodes_.find(node_hash);
456 if (it == nodes_.end()) {
457 return false;
458 }
459 node = it->second;
460 return true;
461}
462
463template <typename LeafValueType>
465{
466 auto it = nodes_by_index_[level].find(index);
467 if (it == nodes_by_index_[level].end()) {
468 return std::nullopt;
469 }
470 return it->second;
471}
472
473template <typename LeafValueType>
475{
476 // If there is no current journal then we just update the cache and leave
477 if (journals_.empty()) {
478 nodes_by_index_[level][index] = node;
479 return;
480 }
481
482 // There is a journal, grab it
483 Journal& journal = journals_.back();
484
485 // If there is no node at the given location then add a nullopt to the journal
486 auto cacheIter = nodes_by_index_[level].find(index);
487 if (cacheIter == nodes_by_index_[level].end()) {
488 journal.nodes_by_index_[level][index] = std::nullopt;
489 } else {
490 // There is a node. If the journal does not have a node at this index then add it to the journal
491 auto journalIter = journal.nodes_by_index_[level].find(index);
492 if (journalIter == journal.nodes_by_index_[level].end()) {
493 journal.nodes_by_index_[level][index] = cacheIter->second;
494 }
495 }
496 nodes_by_index_[level][index] = node;
497}
498} // 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_