Skip to content

Commit 5a04ca8

Browse files
authored
feat: Avoid inserting an empty leaf in indexed trees on update (#10281)
Previously, we were always appending an empty leaf when we encountered an update in the public data tree. After this PR public data tree insertions don't add an empty leaf when the write is an update.
1 parent 2367c62 commit 5a04ca8

File tree

13 files changed

+267
-191
lines changed

13 files changed

+267
-191
lines changed

barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/content_addressed_indexed_tree.hpp

+73-47
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,20 @@ class ContentAddressedIndexedTree : public ContentAddressedAppendOnlyTree<Store,
252252
const InsertionGenerationCallback& completion);
253253

254254
struct InsertionUpdates {
255+
// On insertion, we always update a low leaf. If it's creating a new leaf, we need to update the pointer to
256+
// point to the new one, if it's an update to an existing leaf, we need to change its payload.
255257
LeafUpdate low_leaf_update;
256-
IndexedLeafValueType new_leaf;
257-
index_t new_leaf_index;
258+
// We don't create new leaves on update
259+
std::optional<std::pair<IndexedLeafValueType, index_t>> new_leaf;
258260
};
259261

260262
struct SequentialInsertionGenerationResponse {
261-
std::shared_ptr<std::vector<InsertionUpdates>> updates_to_perform;
263+
std::vector<InsertionUpdates> updates_to_perform;
262264
index_t highest_index;
263265
};
264266

265267
using SequentialInsertionGenerationCallback =
266-
std::function<void(const TypedResponse<SequentialInsertionGenerationResponse>&)>;
268+
std::function<void(TypedResponse<SequentialInsertionGenerationResponse>&)>;
267269
void generate_sequential_insertions(const std::vector<LeafValueType>& values,
268270
const SequentialInsertionGenerationCallback& completion);
269271

@@ -1422,6 +1424,14 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::add_or_update_values_seq
14221424
const AddSequentiallyCompletionCallbackWithWitness& completion,
14231425
bool capture_witness)
14241426
{
1427+
1428+
// This struct is used to collect some state from the asynchronous operations we are about to perform
1429+
struct IntermediateResults {
1430+
std::vector<InsertionUpdates> updates_to_perform;
1431+
size_t appended_leaves = 0;
1432+
};
1433+
auto results = std::make_shared<IntermediateResults>();
1434+
14251435
auto on_error = [=](const std::string& message) {
14261436
try {
14271437
TypedResponse<AddIndexedDataSequentiallyResponse<LeafValueType>> response;
@@ -1443,7 +1453,7 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::add_or_update_values_seq
14431453
ReadTransactionPtr tx = store_->create_read_transaction();
14441454
store_->get_meta(meta, *tx, true);
14451455

1446-
index_t new_total_size = values.size() + meta.size;
1456+
index_t new_total_size = results->appended_leaves + meta.size;
14471457
meta.size = new_total_size;
14481458
meta.root = store_->get_current_root(*tx, true);
14491459

@@ -1452,23 +1462,29 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::add_or_update_values_seq
14521462

14531463
if (capture_witness) {
14541464
// Split results->update_witnesses between low_leaf_witness_data and insertion_witness_data
1455-
// Currently we always insert an empty leaf, even if it's an update, so we can split based
1456-
// on the index of the witness data
14571465
response.inner.insertion_witness_data =
14581466
std::make_shared<std::vector<LeafUpdateWitnessData<LeafValueType>>>();
1459-
;
1467+
response.inner.insertion_witness_data->reserve(results->updates_to_perform.size());
1468+
14601469
response.inner.low_leaf_witness_data =
14611470
std::make_shared<std::vector<LeafUpdateWitnessData<LeafValueType>>>();
1462-
;
1463-
1464-
for (size_t i = 0; i < updates_completion_response.inner.update_witnesses->size(); ++i) {
1465-
LeafUpdateWitnessData<LeafValueType>& witness_data =
1466-
updates_completion_response.inner.update_witnesses->at(i);
1467-
// If even, it's a low leaf, if odd, it's an insertion witness
1468-
if (i % 2 == 0) {
1469-
response.inner.low_leaf_witness_data->push_back(witness_data);
1471+
response.inner.low_leaf_witness_data->reserve(results->updates_to_perform.size());
1472+
1473+
size_t current_witness_index = 0;
1474+
for (size_t i = 0; i < results->updates_to_perform.size(); ++i) {
1475+
LeafUpdateWitnessData<LeafValueType> low_leaf_witness =
1476+
updates_completion_response.inner.update_witnesses->at(current_witness_index++);
1477+
response.inner.low_leaf_witness_data->push_back(low_leaf_witness);
1478+
1479+
// If this update has an insertion, append the real witness
1480+
if (results->updates_to_perform.at(i).new_leaf.has_value()) {
1481+
LeafUpdateWitnessData<LeafValueType> insertion_witness =
1482+
updates_completion_response.inner.update_witnesses->at(current_witness_index++);
1483+
response.inner.insertion_witness_data->push_back(insertion_witness);
14701484
} else {
1471-
response.inner.insertion_witness_data->push_back(witness_data);
1485+
// If it's an update, append an empty witness
1486+
response.inner.insertion_witness_data->push_back(LeafUpdateWitnessData<LeafValueType>{
1487+
.leaf = IndexedLeafValueType::empty(), .index = 0, .path = std::vector<fr>(depth_) });
14721488
}
14731489
}
14741490
}
@@ -1480,23 +1496,33 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::add_or_update_values_seq
14801496
// This signals the completion of the insertion data generation
14811497
// Here we'll perform all updates to the tree
14821498
SequentialInsertionGenerationCallback insertion_generation_completed =
1483-
[=, this](const TypedResponse<SequentialInsertionGenerationResponse>& insertion_response) {
1499+
[=, this](TypedResponse<SequentialInsertionGenerationResponse>& insertion_response) {
14841500
if (!insertion_response.success) {
14851501
on_error(insertion_response.message);
14861502
return;
14871503
}
14881504

14891505
std::shared_ptr<std::vector<LeafUpdate>> flat_updates = std::make_shared<std::vector<LeafUpdate>>();
1490-
for (size_t i = 0; i < insertion_response.inner.updates_to_perform->size(); ++i) {
1491-
InsertionUpdates& insertion_update = insertion_response.inner.updates_to_perform->at(i);
1506+
flat_updates->reserve(insertion_response.inner.updates_to_perform.size() * 2);
1507+
1508+
for (size_t i = 0; i < insertion_response.inner.updates_to_perform.size(); ++i) {
1509+
InsertionUpdates& insertion_update = insertion_response.inner.updates_to_perform.at(i);
14921510
flat_updates->push_back(insertion_update.low_leaf_update);
1493-
flat_updates->push_back(LeafUpdate{
1494-
.leaf_index = insertion_update.new_leaf_index,
1495-
.updated_leaf = insertion_update.new_leaf,
1496-
.original_leaf = IndexedLeafValueType::empty(),
1497-
});
1511+
if (insertion_update.new_leaf.has_value()) {
1512+
results->appended_leaves++;
1513+
IndexedLeafValueType new_leaf;
1514+
index_t new_leaf_index = 0;
1515+
std::tie(new_leaf, new_leaf_index) = insertion_update.new_leaf.value();
1516+
flat_updates->push_back(LeafUpdate{
1517+
.leaf_index = new_leaf_index,
1518+
.updated_leaf = new_leaf,
1519+
.original_leaf = IndexedLeafValueType::empty(),
1520+
});
1521+
}
14981522
}
1499-
1523+
// We won't use anymore updates_to_perform
1524+
results->updates_to_perform = std::move(insertion_response.inner.updates_to_perform);
1525+
assert(insertion_response.inner.updates_to_perform.size() == 0);
15001526
if (capture_witness) {
15011527
perform_updates(flat_updates->size(), flat_updates, final_completion);
15021528
return;
@@ -1514,27 +1540,12 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::generate_sequential_inse
15141540
{
15151541
execute_and_report<SequentialInsertionGenerationResponse>(
15161542
[=, this](TypedResponse<SequentialInsertionGenerationResponse>& response) {
1517-
response.inner.highest_index = 0;
1518-
response.inner.updates_to_perform = std::make_shared<std::vector<InsertionUpdates>>();
1519-
15201543
TreeMeta meta;
15211544
ReadTransactionPtr tx = store_->create_read_transaction();
15221545
store_->get_meta(meta, *tx, true);
15231546

15241547
RequestContext requestContext;
15251548
requestContext.includeUncommitted = true;
1526-
// Ensure that the tree is not going to be overfilled
1527-
index_t new_total_size = values.size() + meta.size;
1528-
if (new_total_size > max_size_) {
1529-
throw std::runtime_error(format("Unable to insert values into tree ",
1530-
meta.name,
1531-
" new size: ",
1532-
new_total_size,
1533-
" max size: ",
1534-
max_size_));
1535-
}
1536-
// The highest index touched will be the last leaf index, since we append a zero for updates
1537-
response.inner.highest_index = new_total_size - 1;
15381549
requestContext.root = store_->get_current_root(*tx, true);
15391550
// Fetch the frontier (non empty nodes to the right) of the tree. This will ensure that perform_updates or
15401551
// perform_updates_without_witness has all the cached nodes it needs to perform the insertions. See comment
@@ -1543,12 +1554,15 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::generate_sequential_inse
15431554
find_leaf_hash(meta.size - 1, requestContext, *tx, true);
15441555
}
15451556

1557+
index_t current_size = meta.size;
1558+
15461559
for (size_t i = 0; i < values.size(); ++i) {
15471560
const LeafValueType& new_payload = values[i];
1561+
// TODO(Alvaro) - Rethink this. I think it's fine for us to interpret empty values as a regular update
1562+
// (it'd empty out the payload of the zero leaf)
15481563
if (new_payload.is_empty()) {
15491564
continue;
15501565
}
1551-
index_t index_of_new_leaf = i + meta.size;
15521566
fr value = new_payload.get_key();
15531567

15541568
// This gives us the leaf that need updating
@@ -1595,25 +1609,25 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::generate_sequential_inse
15951609
.updated_leaf = IndexedLeafValueType::empty(),
15961610
.original_leaf = low_leaf,
15971611
},
1598-
.new_leaf = IndexedLeafValueType::empty(),
1599-
.new_leaf_index = index_of_new_leaf,
1612+
.new_leaf = std::nullopt,
16001613
};
16011614

16021615
if (!is_already_present) {
16031616
// Update the current leaf to point it to the new leaf
16041617
IndexedLeafValueType new_leaf =
16051618
IndexedLeafValueType(new_payload, low_leaf.nextIndex, low_leaf.nextValue);
1606-
1619+
index_t index_of_new_leaf = current_size;
16071620
low_leaf.nextIndex = index_of_new_leaf;
16081621
low_leaf.nextValue = value;
1622+
current_size++;
16091623
// Cache the new leaf
16101624
store_->set_leaf_key_at_index(index_of_new_leaf, new_leaf);
16111625
store_->put_cached_leaf_by_index(index_of_new_leaf, new_leaf);
16121626
// Update cached low leaf
16131627
store_->put_cached_leaf_by_index(low_leaf_index, low_leaf);
16141628

16151629
insertion_update.low_leaf_update.updated_leaf = low_leaf;
1616-
insertion_update.new_leaf = new_leaf;
1630+
insertion_update.new_leaf = std::pair(new_leaf, index_of_new_leaf);
16171631
} else if (IndexedLeafValueType::is_updateable()) {
16181632
// Update the current leaf's value, don't change it's link
16191633
IndexedLeafValueType replacement_leaf =
@@ -1631,8 +1645,20 @@ void ContentAddressedIndexedTree<Store, HashingPolicy>::generate_sequential_inse
16311645
" is already present"));
16321646
}
16331647

1634-
response.inner.updates_to_perform->push_back(insertion_update);
1648+
response.inner.updates_to_perform.push_back(insertion_update);
1649+
}
1650+
1651+
// Ensure that the tree is not going to be overfilled
1652+
if (current_size > max_size_) {
1653+
throw std::runtime_error(format("Unable to insert values into tree ",
1654+
meta.name,
1655+
" new size: ",
1656+
current_size,
1657+
" max size: ",
1658+
max_size_));
16351659
}
1660+
// The highest index touched will be current_size - 1
1661+
response.inner.highest_index = current_size - 1;
16361662
},
16371663
completion);
16381664
}

barretenberg/cpp/src/barretenberg/crypto/merkle_tree/indexed_tree/indexed_leaf.hpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ struct PublicDataLeafValue {
107107

108108
fr get_key() const { return slot; }
109109

110-
bool is_empty() const { return slot == fr::zero(); }
110+
bool is_empty() const { return slot == fr::zero() && value == fr::zero(); }
111111

112112
std::vector<fr> get_hash_inputs(fr nextValue, fr nextIndex) const
113113
{

barretenberg/cpp/src/barretenberg/vm/avm/trace/gadgets/merkle_tree.cpp

+2-6
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,8 @@ FF AvmMerkleTreeTraceBuilder::perform_storage_write([[maybe_unused]] uint32_t cl
102102
// We update the low value
103103
low_preimage.value = value;
104104
FF low_preimage_hash = unconstrained_hash_public_data_preimage(low_preimage);
105-
// Update the low leaf - this will be returned in future
106-
[[maybe_unused]] FF root =
107-
unconstrained_update_leaf_index(low_preimage_hash, static_cast<uint64_t>(low_index), low_path);
108-
// TEMPORARY UNTIL WE CHANGE HOW UPDATES WORK
109-
// Insert a zero leaf at the insertion index
110-
return unconstrained_update_leaf_index(FF::zero(), static_cast<uint64_t>(insertion_index), insertion_path);
105+
// Update the low leaf
106+
return unconstrained_update_leaf_index(low_preimage_hash, static_cast<uint64_t>(low_index), low_path);
111107
}
112108
// The new leaf for an insertion is
113109
PublicDataTreeLeafPreimage new_preimage{

noir-projects/noir-protocol-circuits/crates/rollup-lib/src/base/components/public_data_tree.nr

+5-10
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,11 @@ pub(crate) fn public_data_tree_insert(
5757
},
5858
|write: PublicDataTreeLeaf, low_preimage: PublicDataTreeLeafPreimage| {
5959
// Build insertion leaf
60-
let is_update = low_preimage.slot == write.slot;
61-
if is_update {
62-
PublicDataTreeLeafPreimage::empty()
63-
} else {
64-
PublicDataTreeLeafPreimage {
65-
slot: write.slot,
66-
value: write.value,
67-
next_slot: low_preimage.next_slot,
68-
next_index: low_preimage.next_index,
69-
}
60+
PublicDataTreeLeafPreimage {
61+
slot: write.slot,
62+
value: write.value,
63+
next_slot: low_preimage.next_slot,
64+
next_index: low_preimage.next_index,
7065
}
7166
},
7267
)

noir-projects/noir-protocol-circuits/crates/types/src/data/public_data_tree_leaf.nr

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::traits::Empty;
1+
use crate::{merkle_tree::leaf_preimage::IndexedTreeLeafValue, traits::Empty};
22

33
pub struct PublicDataTreeLeaf {
44
pub slot: Field,
@@ -17,6 +17,12 @@ impl Empty for PublicDataTreeLeaf {
1717
}
1818
}
1919

20+
impl IndexedTreeLeafValue for PublicDataTreeLeaf {
21+
fn get_key(self) -> Field {
22+
self.slot
23+
}
24+
}
25+
2026
impl PublicDataTreeLeaf {
2127
pub fn is_empty(self) -> bool {
2228
(self.slot == 0) & (self.value == 0)

noir-projects/noir-protocol-circuits/crates/types/src/merkle_tree/indexed_tree.nr

+29-23
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod check_valid_low_leaf;
33
use crate::{
44
abis::append_only_tree_snapshot::AppendOnlyTreeSnapshot,
55
merkle_tree::{
6+
leaf_preimage::{IndexedTreeLeafPreimage, IndexedTreeLeafValue},
67
membership::{assert_check_membership, MembershipWitness},
78
root::{calculate_empty_tree_root, calculate_subtree_root, root_from_sibling_path},
89
},
@@ -112,14 +113,14 @@ pub fn insert<Value, Leaf, let TreeHeight: u32>(
112113
build_insertion_leaf: fn(Value, Leaf) -> Leaf,
113114
) -> AppendOnlyTreeSnapshot
114115
where
115-
Value: Eq + Empty,
116-
Leaf: Hash + Empty,
116+
Value: IndexedTreeLeafValue,
117+
Leaf: IndexedTreeLeafPreimage,
117118
{
118119
assert(is_valid_low_leaf(low_leaf_preimage, value), "Invalid low leaf");
119120

120121
// perform membership check for the low leaf against the original root
121122
assert_check_membership(
122-
low_leaf_preimage.hash(),
123+
low_leaf_preimage.as_leaf(),
123124
low_leaf_membership_witness.leaf_index,
124125
low_leaf_membership_witness.sibling_path,
125126
snapshot.root,
@@ -129,29 +130,34 @@ where
129130
let updated_low_leaf =
130131
update_low_leaf(low_leaf_preimage, value, snapshot.next_available_leaf_index);
131132

133+
// Update low leaf
132134
snapshot.root = root_from_sibling_path(
133-
updated_low_leaf.hash(),
135+
updated_low_leaf.as_leaf(),
134136
low_leaf_membership_witness.leaf_index,
135137
low_leaf_membership_witness.sibling_path,
136138
);
137139

138-
let insertion_leaf = build_insertion_leaf(value, low_leaf_preimage);
139-
140-
assert_check_membership(
141-
0,
142-
snapshot.next_available_leaf_index as Field,
143-
insertion_sibling_path,
144-
snapshot.root,
145-
);
146-
147-
// Calculate the new root
148-
snapshot.root = root_from_sibling_path(
149-
insertion_leaf.hash(),
150-
snapshot.next_available_leaf_index as Field,
151-
insertion_sibling_path,
152-
);
153-
154-
snapshot.next_available_leaf_index += 1;
155-
156-
snapshot
140+
if low_leaf_preimage.get_key() == value.get_key() {
141+
// If it's an update, we don't need to insert the new leaf and advance the tree
142+
snapshot
143+
} else {
144+
let insertion_leaf = build_insertion_leaf(value, low_leaf_preimage);
145+
assert_check_membership(
146+
0,
147+
snapshot.next_available_leaf_index as Field,
148+
insertion_sibling_path,
149+
snapshot.root,
150+
);
151+
152+
// Calculate the new root
153+
snapshot.root = root_from_sibling_path(
154+
insertion_leaf.as_leaf(),
155+
snapshot.next_available_leaf_index as Field,
156+
insertion_sibling_path,
157+
);
158+
159+
snapshot.next_available_leaf_index += 1;
160+
161+
snapshot
162+
}
157163
}

noir-projects/noir-protocol-circuits/crates/types/src/merkle_tree/indexed_tree/check_valid_low_leaf.nr

+7
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,19 @@ mod tests {
1616
indexed_tree::check_valid_low_leaf::assert_check_valid_low_leaf,
1717
leaf_preimage::IndexedTreeLeafPreimage,
1818
};
19+
use crate::traits::Empty;
1920

2021
struct TestLeafPreimage {
2122
value: Field,
2223
next_value: Field,
2324
}
2425

26+
impl Empty for TestLeafPreimage {
27+
fn empty() -> Self {
28+
Self { value: 0, next_value: 0 }
29+
}
30+
}
31+
2532
impl IndexedTreeLeafPreimage for TestLeafPreimage {
2633
fn get_key(self) -> Field {
2734
self.value

0 commit comments

Comments
 (0)