monero/tests/unit_tests/curve_trees.cpp

1256 lines
54 KiB
C++
Raw Normal View History

// Copyright (c) 2014, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "gtest/gtest.h"
#include "cryptonote_basic/cryptonote_format_utils.h"
#include "curve_trees.h"
#include "misc_log_ex.h"
#include "ringct/rctOps.h"
#include "unit_tests_utils.h"
2024-06-07 01:48:01 -04:00
#include <algorithm>
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
// CurveTreesGlobalTree helpers
//----------------------------------------------------------------------------------------------------------------------
template<typename C>
static bool validate_layer(const C &curve,
const CurveTreesGlobalTree::Layer<C> &parents,
const std::vector<typename C::Scalar> &child_scalars,
const std::size_t max_chunk_size)
{
// Hash chunk of children scalars, then see if the hash matches up to respective parent
std::size_t chunk_start_idx = 0;
for (std::size_t i = 0; i < parents.size(); ++i)
{
CHECK_AND_ASSERT_MES(child_scalars.size() > chunk_start_idx, false, "chunk start too high");
const std::size_t chunk_size = std::min(child_scalars.size() - chunk_start_idx, max_chunk_size);
CHECK_AND_ASSERT_MES(child_scalars.size() >= (chunk_start_idx + chunk_size), false, "chunk size too large");
const typename C::Point &parent = parents[i];
const auto chunk_start = child_scalars.data() + chunk_start_idx;
const typename C::Chunk chunk{chunk_start, chunk_size};
for (std::size_t i = 0; i < chunk_size; ++i)
MDEBUG("Hashing " << curve.to_string(chunk_start[i]));
const typename C::Point chunk_hash = fcmp::curve_trees::get_new_parent(curve, chunk);
MDEBUG("chunk_start_idx: " << chunk_start_idx << " , chunk_size: " << chunk_size << " , chunk_hash: " << curve.to_string(chunk_hash));
const auto actual_bytes = curve.to_bytes(parent);
const auto expected_bytes = curve.to_bytes(chunk_hash);
CHECK_AND_ASSERT_MES(actual_bytes == expected_bytes, false, "unexpected hash");
chunk_start_idx += chunk_size;
}
CHECK_AND_ASSERT_THROW_MES(chunk_start_idx == child_scalars.size(), "unexpected ending chunk start idx");
return true;
}
//----------------------------------------------------------------------------------------------------------------------
2024-07-09 01:26:53 -04:00
template<typename C_CHILD, typename C_PARENT>
static std::vector<typename C_PARENT::Scalar> get_last_chunk_children_to_trim(const C_CHILD &c_child,
const CurveTreesGlobalTree::Layer<C_CHILD> &child_layer,
const bool need_last_chunk_children_to_trim,
const bool need_last_chunk_remaining_children,
const std::size_t start_trim_idx,
const std::size_t end_trim_idx)
2024-07-09 01:26:53 -04:00
{
std::vector<typename C_PARENT::Scalar> children_to_trim_out;
if (end_trim_idx > start_trim_idx)
2024-07-09 01:26:53 -04:00
{
std::size_t idx = start_trim_idx;
MDEBUG("Start trim from idx: " << idx << " , ending trim at: " << end_trim_idx);
2024-07-09 01:26:53 -04:00
do
{
CHECK_AND_ASSERT_THROW_MES(child_layer.size() > idx, "idx too high");
const auto &child_point = child_layer[idx];
auto child_scalar = c_child.point_to_cycle_scalar(child_point);
children_to_trim_out.push_back(std::move(child_scalar));
++idx;
}
while (idx < end_trim_idx);
2024-07-09 01:26:53 -04:00
}
return children_to_trim_out;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
// CurveTreesGlobalTree implementations
//----------------------------------------------------------------------------------------------------------------------
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
std::size_t CurveTreesGlobalTree::get_num_leaf_tuples() const
{
return m_tree.leaves.size();
}
//----------------------------------------------------------------------------------------------------------------------
CurveTreesV1::LastHashes CurveTreesGlobalTree::get_last_hashes() const
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
CurveTreesV1::LastHashes last_hashes_out;
auto &c1_last_hashes_out = last_hashes_out.c1_last_hashes;
auto &c2_last_hashes_out = last_hashes_out.c2_last_hashes;
const auto &c1_layers = m_tree.c1_layers;
const auto &c2_layers = m_tree.c2_layers;
// We started with c2 and then alternated, so c2 is the same size or 1 higher than c1
CHECK_AND_ASSERT_THROW_MES(c2_layers.size() == c1_layers.size() || c2_layers.size() == (c1_layers.size() + 1),
"unexpected number of curve layers");
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
c1_last_hashes_out.reserve(c1_layers.size());
c2_last_hashes_out.reserve(c2_layers.size());
if (c2_layers.empty())
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
return last_hashes_out;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
// Next parents will be c2
bool use_c2 = true;
// Then get last chunks up until the root
std::size_t c1_idx = 0;
std::size_t c2_idx = 0;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
while (c1_last_hashes_out.size() < c1_layers.size() || c2_last_hashes_out.size() < c2_layers.size())
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
if (use_c2)
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
CHECK_AND_ASSERT_THROW_MES(c2_layers.size() > c2_idx, "missing c2 layer");
c2_last_hashes_out.push_back(c2_layers[c2_idx].back());
++c2_idx;
}
else
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
CHECK_AND_ASSERT_THROW_MES(c1_layers.size() > c1_idx, "missing c1 layer");
c1_last_hashes_out.push_back(c1_layers[c1_idx].back());
++c1_idx;
}
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
use_c2 = !use_c2;
}
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
return last_hashes_out;
}
//----------------------------------------------------------------------------------------------------------------------
void CurveTreesGlobalTree::extend_tree(const CurveTreesV1::TreeExtension &tree_extension)
{
// Add the leaves
CHECK_AND_ASSERT_THROW_MES(m_tree.leaves.size() == tree_extension.leaves.start_leaf_tuple_idx,
"unexpected leaf start idx");
m_tree.leaves.reserve(m_tree.leaves.size() + tree_extension.leaves.tuples.size());
for (const auto &preprocessed_leaf_tuple : tree_extension.leaves.tuples)
{
auto leaf = m_curve_trees.leaf_tuple(preprocessed_leaf_tuple);
m_tree.leaves.emplace_back(CurveTreesV1::LeafTuple{
.O_x = std::move(leaf.O_x),
.I_x = std::move(leaf.I_x),
.C_x = std::move(leaf.C_x)
});
}
// Add the layers
const auto &c2_extensions = tree_extension.c2_layer_extensions;
const auto &c1_extensions = tree_extension.c1_layer_extensions;
CHECK_AND_ASSERT_THROW_MES(!c2_extensions.empty(), "empty c2 extensions");
bool use_c2 = true;
std::size_t c2_idx = 0;
std::size_t c1_idx = 0;
for (std::size_t i = 0; i < (c2_extensions.size() + c1_extensions.size()); ++i)
{
// TODO: template below if statement
if (use_c2)
{
CHECK_AND_ASSERT_THROW_MES(c2_idx < c2_extensions.size(), "unexpected c2 layer extension");
const fcmp::curve_trees::LayerExtension<Selene> &c2_ext = c2_extensions[c2_idx];
CHECK_AND_ASSERT_THROW_MES(!c2_ext.hashes.empty(), "empty c2 layer extension");
CHECK_AND_ASSERT_THROW_MES(c2_idx <= m_tree.c2_layers.size(), "missing c2 layer");
if (m_tree.c2_layers.size() == c2_idx)
m_tree.c2_layers.emplace_back(Layer<Selene>{});
auto &c2_inout = m_tree.c2_layers[c2_idx];
const bool started_after_tip = (c2_inout.size() == c2_ext.start_idx);
const bool started_at_tip = (c2_inout.size() == (c2_ext.start_idx + 1));
CHECK_AND_ASSERT_THROW_MES(started_after_tip || started_at_tip, "unexpected c2 layer start");
// We updated the last hash
if (started_at_tip)
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
{
CHECK_AND_ASSERT_THROW_MES(c2_ext.update_existing_last_hash, "expect to be updating last hash");
c2_inout.back() = c2_ext.hashes.front();
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
}
else
{
CHECK_AND_ASSERT_THROW_MES(!c2_ext.update_existing_last_hash, "unexpected last hash update");
}
for (std::size_t i = started_at_tip ? 1 : 0; i < c2_ext.hashes.size(); ++i)
c2_inout.emplace_back(c2_ext.hashes[i]);
++c2_idx;
}
else
{
CHECK_AND_ASSERT_THROW_MES(c1_idx < c1_extensions.size(), "unexpected c1 layer extension");
const fcmp::curve_trees::LayerExtension<Helios> &c1_ext = c1_extensions[c1_idx];
CHECK_AND_ASSERT_THROW_MES(!c1_ext.hashes.empty(), "empty c1 layer extension");
CHECK_AND_ASSERT_THROW_MES(c1_idx <= m_tree.c1_layers.size(), "missing c1 layer");
if (m_tree.c1_layers.size() == c1_idx)
m_tree.c1_layers.emplace_back(Layer<Helios>{});
auto &c1_inout = m_tree.c1_layers[c1_idx];
const bool started_after_tip = (c1_inout.size() == c1_ext.start_idx);
const bool started_at_tip = (c1_inout.size() == (c1_ext.start_idx + 1));
CHECK_AND_ASSERT_THROW_MES(started_after_tip || started_at_tip, "unexpected c1 layer start");
// We updated the last hash
if (started_at_tip)
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
{
CHECK_AND_ASSERT_THROW_MES(c1_ext.update_existing_last_hash, "expect to be updating last hash");
c1_inout.back() = c1_ext.hashes.front();
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
}
else
{
CHECK_AND_ASSERT_THROW_MES(!c1_ext.update_existing_last_hash, "unexpected last hash update");
}
for (std::size_t i = started_at_tip ? 1 : 0; i < c1_ext.hashes.size(); ++i)
c1_inout.emplace_back(c1_ext.hashes[i]);
++c1_idx;
}
use_c2 = !use_c2;
}
}
//----------------------------------------------------------------------------------------------------------------------
void CurveTreesGlobalTree::reduce_tree(const CurveTreesV1::TreeReduction &tree_reduction)
2024-06-07 01:48:01 -04:00
{
// Trim the leaves
CHECK_AND_ASSERT_THROW_MES(m_tree.leaves.size() > tree_reduction.new_total_leaf_tuples,
"expected fewer new total leaves");
while (m_tree.leaves.size() > tree_reduction.new_total_leaf_tuples)
m_tree.leaves.pop_back();
// Trim the layers
const auto &c2_layer_reductions = tree_reduction.c2_layer_reductions;
const auto &c1_layer_reductions = tree_reduction.c1_layer_reductions;
CHECK_AND_ASSERT_THROW_MES(!c2_layer_reductions.empty(), "empty c2 layer reductions");
2024-06-07 01:48:01 -04:00
bool use_c2 = true;
std::size_t c2_idx = 0;
std::size_t c1_idx = 0;
for (std::size_t i = 0; i < (c2_layer_reductions.size() + c1_layer_reductions.size()); ++i)
{
// TODO: template below if statement
if (use_c2)
{
CHECK_AND_ASSERT_THROW_MES(c2_idx < c2_layer_reductions.size(), "unexpected c2 layer reduction");
const auto &c2_reduction = c2_layer_reductions[c2_idx];
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(c2_idx < m_tree.c2_layers.size(), "missing c2 layer");
auto &c2_inout = m_tree.c2_layers[c2_idx];
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(c2_reduction.new_total_parents <= c2_inout.size(),
"unexpected c2 new total parents");
2024-06-07 01:48:01 -04:00
c2_inout.resize(c2_reduction.new_total_parents);
c2_inout.shrink_to_fit();
2024-06-07 01:48:01 -04:00
// We updated the last hash
if (c2_reduction.update_existing_last_hash)
{
c2_inout.back() = c2_reduction.new_last_hash;
}
2024-06-07 01:48:01 -04:00
++c2_idx;
}
else
{
CHECK_AND_ASSERT_THROW_MES(c1_idx < c1_layer_reductions.size(), "unexpected c1 layer reduction");
const auto &c1_reduction = c1_layer_reductions[c1_idx];
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(c1_idx < m_tree.c1_layers.size(), "missing c1 layer");
auto &c1_inout = m_tree.c1_layers[c1_idx];
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(c1_reduction.new_total_parents <= c1_inout.size(),
"unexpected c1 new total parents");
2024-06-07 01:48:01 -04:00
c1_inout.resize(c1_reduction.new_total_parents);
c1_inout.shrink_to_fit();
2024-06-07 01:48:01 -04:00
// We updated the last hash
if (c1_reduction.update_existing_last_hash)
{
c1_inout.back() = c1_reduction.new_last_hash;
}
2024-06-07 01:48:01 -04:00
++c1_idx;
2024-06-07 01:48:01 -04:00
}
use_c2 = !use_c2;
2024-06-07 01:48:01 -04:00
}
// Delete remaining layers
m_tree.c1_layers.resize(c1_layer_reductions.size());
m_tree.c2_layers.resize(c2_layer_reductions.size());
2024-06-07 01:48:01 -04:00
m_tree.c1_layers.shrink_to_fit();
m_tree.c2_layers.shrink_to_fit();
}
//----------------------------------------------------------------------------------------------------------------------
// TODO: template
CurveTreesV1::LastChunkChildrenToTrim CurveTreesGlobalTree::get_all_last_chunk_children_to_trim(
const std::vector<fcmp::curve_trees::TrimLayerInstructions> &trim_instructions)
{
CurveTreesV1::LastChunkChildrenToTrim all_children_to_trim;
2024-06-07 01:48:01 -04:00
// Leaf layer
CHECK_AND_ASSERT_THROW_MES(!trim_instructions.empty(), "no instructions");
const auto &trim_leaf_layer_instructions = trim_instructions[0];
2024-06-07 01:48:01 -04:00
std::vector<Selene::Scalar> leaves_to_trim;
2024-06-07 01:48:01 -04:00
// TODO: separate function
if (trim_leaf_layer_instructions.end_trim_idx > trim_leaf_layer_instructions.start_trim_idx)
2024-06-07 01:48:01 -04:00
{
std::size_t idx = trim_leaf_layer_instructions.start_trim_idx;
MDEBUG("Start trim from idx: " << idx);
do
2024-06-07 01:48:01 -04:00
{
CHECK_AND_ASSERT_THROW_MES(idx % CurveTreesV1::LEAF_TUPLE_SIZE == 0, "expected divisble by leaf tuple size");
const std::size_t leaf_tuple_idx = idx / CurveTreesV1::LEAF_TUPLE_SIZE;
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(m_tree.leaves.size() > leaf_tuple_idx, "leaf_tuple_idx too high");
2024-07-08 23:01:45 -04:00
const auto &leaf_tuple = m_tree.leaves[leaf_tuple_idx];
2024-06-07 01:48:01 -04:00
leaves_to_trim.push_back(leaf_tuple.O_x);
leaves_to_trim.push_back(leaf_tuple.I_x);
leaves_to_trim.push_back(leaf_tuple.C_x);
2024-06-07 01:48:01 -04:00
idx += CurveTreesV1::LEAF_TUPLE_SIZE;
}
while (idx < trim_leaf_layer_instructions.end_trim_idx);
2024-06-07 01:48:01 -04:00
}
all_children_to_trim.c2_children.emplace_back(std::move(leaves_to_trim));
2024-06-07 01:48:01 -04:00
bool parent_is_c2 = false;
std::size_t c1_idx = 0;
std::size_t c2_idx = 0;
for (std::size_t i = 1; i < trim_instructions.size(); ++i)
2024-06-07 01:48:01 -04:00
{
MDEBUG("Getting trim instructions for layer " << i);
const auto &trim_layer_instructions = trim_instructions[i];
2024-06-07 01:48:01 -04:00
const bool need_last_chunk_children_to_trim = trim_layer_instructions.need_last_chunk_children_to_trim;
const bool need_last_chunk_remaining_children = trim_layer_instructions.need_last_chunk_remaining_children;
const std::size_t start_trim_idx = trim_layer_instructions.start_trim_idx;
const std::size_t end_trim_idx = trim_layer_instructions.end_trim_idx;
if (parent_is_c2)
{
CHECK_AND_ASSERT_THROW_MES(m_tree.c1_layers.size() > c1_idx, "c1_idx too high");
2024-06-07 01:48:01 -04:00
auto children_to_trim = get_last_chunk_children_to_trim<Helios, Selene>(
m_curve_trees.m_c1,
m_tree.c1_layers[c1_idx],
need_last_chunk_children_to_trim,
need_last_chunk_remaining_children,
start_trim_idx,
end_trim_idx);
2024-06-07 01:48:01 -04:00
all_children_to_trim.c2_children.emplace_back(std::move(children_to_trim));
++c1_idx;
}
else
{
CHECK_AND_ASSERT_THROW_MES(m_tree.c2_layers.size() > c2_idx, "c2_idx too high");
2024-06-07 01:48:01 -04:00
auto children_to_trim = get_last_chunk_children_to_trim<Selene, Helios>(
m_curve_trees.m_c2,
m_tree.c2_layers[c2_idx],
need_last_chunk_children_to_trim,
need_last_chunk_remaining_children,
start_trim_idx,
end_trim_idx);
2024-06-07 01:48:01 -04:00
all_children_to_trim.c1_children.emplace_back(std::move(children_to_trim));
++c2_idx;
}
2024-06-07 01:48:01 -04:00
parent_is_c2 = !parent_is_c2;
2024-06-07 01:48:01 -04:00
}
return all_children_to_trim;
}
//----------------------------------------------------------------------------------------------------------------------
CurveTreesV1::LastHashes CurveTreesGlobalTree::get_last_hashes_to_trim(
const std::vector<fcmp::curve_trees::TrimLayerInstructions> &trim_instructions) const
{
CurveTreesV1::LastHashes last_hashes;
CHECK_AND_ASSERT_THROW_MES(!trim_instructions.empty(), "no instructions");
2024-06-07 01:48:01 -04:00
bool parent_is_c2 = true;
2024-06-07 01:48:01 -04:00
std::size_t c1_idx = 0;
std::size_t c2_idx = 0;
for (const auto &trim_layer_instructions : trim_instructions)
2024-06-07 01:48:01 -04:00
{
const std::size_t new_total_parents = trim_layer_instructions.new_total_parents;
CHECK_AND_ASSERT_THROW_MES(new_total_parents > 0, "no new parents");
2024-06-07 01:48:01 -04:00
if (parent_is_c2)
2024-06-07 01:48:01 -04:00
{
CHECK_AND_ASSERT_THROW_MES(m_tree.c2_layers.size() > c2_idx, "c2_idx too high");
const auto &c2_layer = m_tree.c2_layers[c2_idx];
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(c2_layer.size() >= new_total_parents, "not enough c2 parents");
last_hashes.c2_last_hashes.push_back(c2_layer[new_total_parents - 1]);
2024-06-07 01:48:01 -04:00
++c2_idx;
}
else
{
CHECK_AND_ASSERT_THROW_MES(m_tree.c1_layers.size() > c1_idx, "c1_idx too high");
const auto &c1_layer = m_tree.c1_layers[c1_idx];
2024-06-07 01:48:01 -04:00
CHECK_AND_ASSERT_THROW_MES(c1_layer.size() >= new_total_parents, "not enough c1 parents");
last_hashes.c1_last_hashes.push_back(c1_layer[new_total_parents - 1]);
2024-06-07 01:48:01 -04:00
++c1_idx;
}
parent_is_c2 = !parent_is_c2;
2024-06-07 01:48:01 -04:00
}
return last_hashes;
}
//----------------------------------------------------------------------------------------------------------------------
void CurveTreesGlobalTree::trim_tree(const std::size_t trim_n_leaf_tuples)
{
const std::size_t old_n_leaf_tuples = this->get_num_leaf_tuples();
MDEBUG(old_n_leaf_tuples << " leaves in the tree, trimming " << trim_n_leaf_tuples);
2024-07-08 23:01:45 -04:00
// Get trim instructions
const auto trim_instructions = m_curve_trees.get_trim_instructions(old_n_leaf_tuples, trim_n_leaf_tuples);
MDEBUG("Acquired trim instructions for " << trim_instructions.size() << " layers");
// Do initial tree reads
const auto last_chunk_children_to_trim = this->get_all_last_chunk_children_to_trim(trim_instructions);
const auto last_hashes_to_trim = this->get_last_hashes_to_trim(trim_instructions);
2024-07-08 23:01:45 -04:00
// Get the new hashes, wrapped in a simple struct we can use to trim the tree
const auto tree_reduction = m_curve_trees.get_tree_reduction(
trim_instructions,
last_chunk_children_to_trim,
last_hashes_to_trim);
// Use tree reduction to trim tree
this->reduce_tree(tree_reduction);
const std::size_t new_n_leaf_tuples = this->get_num_leaf_tuples();
CHECK_AND_ASSERT_THROW_MES((new_n_leaf_tuples + trim_n_leaf_tuples) == old_n_leaf_tuples,
"unexpected num leaves after trim");
2024-06-07 01:48:01 -04:00
}
//----------------------------------------------------------------------------------------------------------------------
bool CurveTreesGlobalTree::audit_tree(const std::size_t expected_n_leaf_tuples)
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
MDEBUG("Auditing global tree");
const auto &leaves = m_tree.leaves;
const auto &c1_layers = m_tree.c1_layers;
const auto &c2_layers = m_tree.c2_layers;
CHECK_AND_ASSERT_MES(!leaves.empty(), false, "must have at least 1 leaf in tree");
CHECK_AND_ASSERT_MES(leaves.size() == expected_n_leaf_tuples, false, "unexpected num leaves");
CHECK_AND_ASSERT_MES(!c2_layers.empty(), false, "must have at least 1 c2 layer in tree");
CHECK_AND_ASSERT_MES(c2_layers.size() == c1_layers.size() || c2_layers.size() == (c1_layers.size() + 1),
false, "unexpected mismatch of c2 and c1 layers");
// Verify root has 1 member in it
const bool c2_is_root = c2_layers.size() > c1_layers.size();
CHECK_AND_ASSERT_MES(c2_is_root ? c2_layers.back().size() == 1 : c1_layers.back().size() == 1, false,
"root must have 1 member in it");
// Iterate from root down to layer above leaves, and check hashes match up correctly
bool parent_is_c2 = c2_is_root;
std::size_t c2_idx = c2_layers.size() - 1;
std::size_t c1_idx = c1_layers.empty() ? 0 : (c1_layers.size() - 1);
for (std::size_t i = 1; i < (c2_layers.size() + c1_layers.size()); ++i)
{
// TODO: implement templated function for below if statement
if (parent_is_c2)
{
MDEBUG("Validating parent c2 layer " << c2_idx << " , child c1 layer " << c1_idx);
CHECK_AND_ASSERT_THROW_MES(c2_idx < c2_layers.size(), "unexpected c2_idx");
CHECK_AND_ASSERT_THROW_MES(c1_idx < c1_layers.size(), "unexpected c1_idx");
const Layer<Selene> &parents = c2_layers[c2_idx];
const Layer<Helios> &children = c1_layers[c1_idx];
CHECK_AND_ASSERT_MES(!parents.empty(), false, "no parents at c2_idx " + std::to_string(c2_idx));
CHECK_AND_ASSERT_MES(!children.empty(), false, "no children at c1_idx " + std::to_string(c1_idx));
std::vector<Selene::Scalar> child_scalars;
fcmp::tower_cycle::extend_scalars_from_cycle_points<Helios, Selene>(m_curve_trees.m_c1,
children,
child_scalars);
const bool valid = validate_layer<Selene>(m_curve_trees.m_c2,
parents,
child_scalars,
m_curve_trees.m_c2_width);
CHECK_AND_ASSERT_MES(valid, false, "failed to validate c2_idx " + std::to_string(c2_idx));
--c2_idx;
}
else
{
MDEBUG("Validating parent c1 layer " << c1_idx << " , child c2 layer " << c2_idx);
CHECK_AND_ASSERT_THROW_MES(c1_idx < c1_layers.size(), "unexpected c1_idx");
CHECK_AND_ASSERT_THROW_MES(c2_idx < c2_layers.size(), "unexpected c2_idx");
const Layer<Helios> &parents = c1_layers[c1_idx];
const Layer<Selene> &children = c2_layers[c2_idx];
CHECK_AND_ASSERT_MES(!parents.empty(), false, "no parents at c1_idx " + std::to_string(c1_idx));
CHECK_AND_ASSERT_MES(!children.empty(), false, "no children at c2_idx " + std::to_string(c2_idx));
std::vector<Helios::Scalar> child_scalars;
fcmp::tower_cycle::extend_scalars_from_cycle_points<Selene, Helios>(m_curve_trees.m_c2,
children,
child_scalars);
const bool valid = validate_layer<Helios>(
m_curve_trees.m_c1,
parents,
child_scalars,
m_curve_trees.m_c1_width);
CHECK_AND_ASSERT_MES(valid, false, "failed to validate c1_idx " + std::to_string(c1_idx));
--c1_idx;
}
parent_is_c2 = !parent_is_c2;
}
MDEBUG("Validating leaves");
// Now validate leaves
return validate_layer<Selene>(m_curve_trees.m_c2,
c2_layers[0],
m_curve_trees.flatten_leaves(leaves),
m_curve_trees.m_leaf_layer_chunk_width);
}
//----------------------------------------------------------------------------------------------------------------------
// Logging helpers
//----------------------------------------------------------------------------------------------------------------------
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
void CurveTreesGlobalTree::log_last_hashes(const CurveTreesV1::LastHashes &last_hashes)
{
if (!el::Loggers::allowed(el::Level::Debug, "serialization"))
return;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
const auto &c1_last_hashes = last_hashes.c1_last_hashes;
const auto &c2_last_hashes = last_hashes.c2_last_hashes;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
MDEBUG("Total of " << c1_last_hashes.size() << " Helios layers and " << c2_last_hashes.size() << " Selene layers");
bool use_c2 = true;
std::size_t c1_idx = 0;
std::size_t c2_idx = 0;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
for (std::size_t i = 0; i < (c1_last_hashes.size() + c2_last_hashes.size()); ++i)
{
if (use_c2)
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
CHECK_AND_ASSERT_THROW_MES(c2_idx < c2_last_hashes.size(), "unexpected c2 layer");
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
const auto &last_hash = c2_last_hashes[c2_idx];
MDEBUG("c2_idx: " << c2_idx << " , last_hash: " << m_curve_trees.m_c2.to_string(last_hash));
++c2_idx;
}
else
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
CHECK_AND_ASSERT_THROW_MES(c1_idx < c1_last_hashes.size(), "unexpected c1 layer");
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
const auto &last_hash = c1_last_hashes[c1_idx];
MDEBUG("c1_idx: " << c1_idx << " , last_hash: " << m_curve_trees.m_c1.to_string(last_hash));
++c1_idx;
}
use_c2 = !use_c2;
}
}
//----------------------------------------------------------------------------------------------------------------------
void CurveTreesGlobalTree::log_tree_extension(const CurveTreesV1::TreeExtension &tree_extension)
{
if (!el::Loggers::allowed(el::Level::Debug, "serialization"))
return;
const auto &c1_extensions = tree_extension.c1_layer_extensions;
const auto &c2_extensions = tree_extension.c2_layer_extensions;
MDEBUG("Tree extension has " << tree_extension.leaves.tuples.size() << " leaves, "
<< c1_extensions.size() << " helios layers, " << c2_extensions.size() << " selene layers");
MDEBUG("Leaf start idx: " << tree_extension.leaves.start_leaf_tuple_idx);
for (std::size_t i = 0; i < tree_extension.leaves.tuples.size(); ++i)
{
const auto &preprocessed_leaf_tuple = tree_extension.leaves.tuples[i];
const auto leaf = m_curve_trees.leaf_tuple(preprocessed_leaf_tuple);
const auto O_x = m_curve_trees.m_c2.to_string(leaf.O_x);
const auto I_x = m_curve_trees.m_c2.to_string(leaf.I_x);
const auto C_x = m_curve_trees.m_c2.to_string(leaf.C_x);
MDEBUG("Leaf tuple idx " << (tree_extension.leaves.start_leaf_tuple_idx)
2024-05-20 18:24:27 -04:00
<< " : { O_x: " << O_x << " , I_x: " << I_x << " , C_x: " << C_x << " }");
}
bool use_c2 = true;
std::size_t c1_idx = 0;
std::size_t c2_idx = 0;
for (std::size_t i = 0; i < (c1_extensions.size() + c2_extensions.size()); ++i)
{
if (use_c2)
{
CHECK_AND_ASSERT_THROW_MES(c2_idx < c2_extensions.size(), "unexpected c2 layer");
const fcmp::curve_trees::LayerExtension<Selene> &c2_layer = c2_extensions[c2_idx];
MDEBUG("Selene tree extension start idx: " << c2_layer.start_idx);
for (std::size_t j = 0; j < c2_layer.hashes.size(); ++j)
MDEBUG("Child chunk start idx: " << (j + c2_layer.start_idx) << " , hash: "
<< m_curve_trees.m_c2.to_string(c2_layer.hashes[j]));
++c2_idx;
}
else
{
CHECK_AND_ASSERT_THROW_MES(c1_idx < c1_extensions.size(), "unexpected c1 layer");
const fcmp::curve_trees::LayerExtension<Helios> &c1_layer = c1_extensions[c1_idx];
MDEBUG("Helios tree extension start idx: " << c1_layer.start_idx);
for (std::size_t j = 0; j < c1_layer.hashes.size(); ++j)
MDEBUG("Child chunk start idx: " << (j + c1_layer.start_idx) << " , hash: "
<< m_curve_trees.m_c1.to_string(c1_layer.hashes[j]));
++c1_idx;
}
use_c2 = !use_c2;
}
}
//----------------------------------------------------------------------------------------------------------------------
void CurveTreesGlobalTree::log_tree()
{
if (!el::Loggers::allowed(el::Level::Debug, "serialization"))
return;
MDEBUG("Tree has " << m_tree.leaves.size() << " leaves, "
<< m_tree.c1_layers.size() << " helios layers, " << m_tree.c2_layers.size() << " selene layers");
for (std::size_t i = 0; i < m_tree.leaves.size(); ++i)
{
const auto &leaf = m_tree.leaves[i];
const auto O_x = m_curve_trees.m_c2.to_string(leaf.O_x);
const auto I_x = m_curve_trees.m_c2.to_string(leaf.I_x);
const auto C_x = m_curve_trees.m_c2.to_string(leaf.C_x);
MDEBUG("Leaf idx " << i << " : { O_x: " << O_x << " , I_x: " << I_x << " , C_x: " << C_x << " }");
}
bool use_c2 = true;
std::size_t c1_idx = 0;
std::size_t c2_idx = 0;
for (std::size_t i = 0; i < (m_tree.c1_layers.size() + m_tree.c2_layers.size()); ++i)
{
if (use_c2)
{
CHECK_AND_ASSERT_THROW_MES(c2_idx < m_tree.c2_layers.size(), "unexpected c2 layer");
const CurveTreesGlobalTree::Layer<Selene> &c2_layer = m_tree.c2_layers[c2_idx];
MDEBUG("Selene layer size: " << c2_layer.size() << " , tree layer: " << i);
for (std::size_t j = 0; j < c2_layer.size(); ++j)
MDEBUG("Child chunk start idx: " << j << " , hash: " << m_curve_trees.m_c2.to_string(c2_layer[j]));
++c2_idx;
}
else
{
CHECK_AND_ASSERT_THROW_MES(c1_idx < m_tree.c1_layers.size(), "unexpected c1 layer");
const CurveTreesGlobalTree::Layer<Helios> &c1_layer = m_tree.c1_layers[c1_idx];
MDEBUG("Helios layer size: " << c1_layer.size() << " , tree layer: " << i);
for (std::size_t j = 0; j < c1_layer.size(); ++j)
MDEBUG("Child chunk start idx: " << j << " , hash: " << m_curve_trees.m_c1.to_string(c1_layer[j]));
++c1_idx;
}
use_c2 = !use_c2;
}
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
// Test helpers
//----------------------------------------------------------------------------------------------------------------------
static const std::vector<fcmp::curve_trees::LeafTupleContext> generate_random_leaves(const CurveTreesV1 &curve_trees,
const std::size_t old_n_leaf_tuples,
const std::size_t new_n_leaf_tuples)
{
std::vector<fcmp::curve_trees::LeafTupleContext> tuples;
tuples.reserve(new_n_leaf_tuples);
for (std::size_t i = 0; i < new_n_leaf_tuples; ++i)
{
const std::uint64_t output_id = old_n_leaf_tuples + i;
// Generate random output tuple
crypto::secret_key o,c;
crypto::public_key O,C;
crypto::generate_keys(O, o, o, false);
crypto::generate_keys(C, c, c, false);
auto tuple_context = curve_trees.output_to_leaf_context(output_id, O, rct::pk2rct(C));
tuples.emplace_back(std::move(tuple_context));
}
return tuples;
}
//----------------------------------------------------------------------------------------------------------------------
static const Selene::Scalar generate_random_selene_scalar()
{
crypto::secret_key s;
crypto::public_key S;
crypto::generate_keys(S, s, s, false);
rct::key S_x;
CHECK_AND_ASSERT_THROW_MES(rct::point_to_wei_x(rct::pk2rct(S), S_x), "failed to convert to wei x");
return fcmp::tower_cycle::selene_scalar_from_bytes(S_x);
}
//----------------------------------------------------------------------------------------------------------------------
static bool grow_tree(CurveTreesV1 &curve_trees,
CurveTreesGlobalTree &global_tree,
const std::size_t new_n_leaf_tuples)
{
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
// Do initial tree reads
const std::size_t old_n_leaf_tuples = global_tree.get_num_leaf_tuples();
const CurveTreesV1::LastHashes last_hashes = global_tree.get_last_hashes();
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
global_tree.log_last_hashes(last_hashes);
auto new_leaf_tuples = generate_random_leaves(curve_trees, old_n_leaf_tuples, new_n_leaf_tuples);
// Get a tree extension object to the existing tree using randomly generated leaves
// - The tree extension includes all elements we'll need to add to the existing tree when adding the new leaves
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
const auto tree_extension = curve_trees.get_tree_extension(old_n_leaf_tuples,
last_hashes,
std::move(new_leaf_tuples));
global_tree.log_tree_extension(tree_extension);
// Use the tree extension to extend the existing tree
global_tree.extend_tree(tree_extension);
global_tree.log_tree();
// Validate tree structure and all hashes
const std::size_t expected_n_leaf_tuples = old_n_leaf_tuples + new_n_leaf_tuples;
return global_tree.audit_tree(expected_n_leaf_tuples);
}
//----------------------------------------------------------------------------------------------------------------------
static bool grow_tree_in_memory(const std::size_t init_leaves,
const std::size_t ext_leaves,
CurveTreesV1 &curve_trees)
{
LOG_PRINT_L1("Adding " << init_leaves << " leaves to tree in memory, then extending by "
<< ext_leaves << " leaves");
CurveTreesGlobalTree global_tree(curve_trees);
// Initialize global tree with `init_leaves`
MDEBUG("Adding " << init_leaves << " leaves to tree");
2024-06-03 20:15:02 -04:00
bool res = grow_tree(curve_trees, global_tree, init_leaves);
CHECK_AND_ASSERT_MES(res, false, "failed to add inital leaves to tree in memory");
MDEBUG("Successfully added initial " << init_leaves << " leaves to tree in memory");
// Then extend the global tree by `ext_leaves`
MDEBUG("Extending tree by " << ext_leaves << " leaves");
2024-06-03 20:15:02 -04:00
res = grow_tree(curve_trees, global_tree, ext_leaves);
CHECK_AND_ASSERT_MES(res, false, "failed to extend tree in memory");
MDEBUG("Successfully extended by " << ext_leaves << " leaves in memory");
return true;
}
//----------------------------------------------------------------------------------------------------------------------
static bool trim_tree_in_memory(const std::size_t trim_n_leaf_tuples,
2024-06-07 02:47:29 -04:00
CurveTreesGlobalTree &&global_tree)
2024-06-03 20:15:02 -04:00
{
const std::size_t old_n_leaf_tuples = global_tree.get_num_leaf_tuples();
CHECK_AND_ASSERT_THROW_MES(old_n_leaf_tuples > trim_n_leaf_tuples, "cannot trim more leaves than exist");
CHECK_AND_ASSERT_THROW_MES(trim_n_leaf_tuples > 0, "must be trimming some leaves");
// Trim the global tree by `trim_n_leaf_tuples`
LOG_PRINT_L1("Trimming " << trim_n_leaf_tuples << " leaf tuples from tree with "
<< old_n_leaf_tuples << " leaves in memory");
2024-06-03 20:15:02 -04:00
global_tree.trim_tree(trim_n_leaf_tuples);
2024-06-07 01:48:01 -04:00
MDEBUG("Finished trimming " << trim_n_leaf_tuples << " leaf tuples from tree");
2024-06-07 01:48:01 -04:00
global_tree.log_tree();
2024-06-03 20:15:02 -04:00
const std::size_t expected_n_leaf_tuples = old_n_leaf_tuples - trim_n_leaf_tuples;
bool res = global_tree.audit_tree(expected_n_leaf_tuples);
2024-06-03 20:15:02 -04:00
CHECK_AND_ASSERT_MES(res, false, "failed to trim tree in memory");
MDEBUG("Successfully trimmed " << trim_n_leaf_tuples << " leaves in memory");
2024-06-03 20:15:02 -04:00
return true;
}
//----------------------------------------------------------------------------------------------------------------------
static bool grow_tree_db(const std::size_t init_leaves,
const std::size_t ext_leaves,
CurveTreesV1 &curve_trees,
unit_test::BlockchainLMDBTest &test_db)
{
INIT_BLOCKCHAIN_LMDB_TEST_DB(&curve_trees);
{
cryptonote::db_wtxn_guard guard(test_db.m_db);
LOG_PRINT_L1("Adding " << init_leaves << " leaves to db, then extending by " << ext_leaves << " leaves");
auto init_leaf_tuples = generate_random_leaves(curve_trees, 0, init_leaves);
test_db.m_db->grow_tree(std::move(init_leaf_tuples));
CHECK_AND_ASSERT_MES(test_db.m_db->audit_tree(init_leaves), false,
"failed to add initial leaves to db");
MDEBUG("Successfully added initial " << init_leaves << " leaves to db, extending by "
<< ext_leaves << " leaves");
auto ext_leaf_tuples = generate_random_leaves(curve_trees, init_leaves, ext_leaves);
test_db.m_db->grow_tree(std::move(ext_leaf_tuples));
CHECK_AND_ASSERT_MES(test_db.m_db->audit_tree(init_leaves + ext_leaves), false,
"failed to extend tree in db");
MDEBUG("Successfully extended tree in db by " << ext_leaves << " leaves");
}
return true;
}
//----------------------------------------------------------------------------------------------------------------------
static bool trim_tree_db(const std::size_t init_leaves,
const std::size_t trim_leaves,
CurveTreesV1 &curve_trees,
unit_test::BlockchainLMDBTest &test_db)
{
INIT_BLOCKCHAIN_LMDB_TEST_DB(&curve_trees);
{
cryptonote::db_wtxn_guard guard(test_db.m_db);
LOG_PRINT_L1("Adding " << init_leaves << " leaves to db, then trimming by " << trim_leaves << " leaves");
auto init_leaf_tuples = generate_random_leaves(curve_trees, 0, init_leaves);
test_db.m_db->grow_tree(std::move(init_leaf_tuples));
CHECK_AND_ASSERT_MES(test_db.m_db->audit_tree(init_leaves), false,
"failed to add initial leaves to db");
MDEBUG("Successfully added initial " << init_leaves << " leaves to db, trimming by "
<< trim_leaves << " leaves");
test_db.m_db->trim_tree(trim_leaves);
CHECK_AND_ASSERT_MES(test_db.m_db->audit_tree(init_leaves - trim_leaves), false,
"failed to trim tree in db");
MDEBUG("Successfully trimmed tree in db by " << trim_leaves << " leaves");
}
return true;
}
//----------------------------------------------------------------------------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------
// Test
//----------------------------------------------------------------------------------------------------------------------
TEST(curve_trees, grow_tree)
{
Helios helios;
Selene selene;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
// Use lower values for chunk width than prod so that we can quickly test a many-layer deep tree
static const std::size_t helios_chunk_width = 3;
static const std::size_t selene_chunk_width = 2;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
static_assert(helios_chunk_width > 1, "helios width must be > 1");
static_assert(selene_chunk_width > 1, "selene width must be > 1");
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
LOG_PRINT_L1("Test grow tree with helios chunk width " << helios_chunk_width
<< ", selene chunk width " << selene_chunk_width);
// Constant for how deep we want the tree
static const std::size_t TEST_N_LAYERS = 4;
// Number of leaves for which x number of layers is required
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
std::size_t leaves_needed_for_n_layers = selene_chunk_width;
for (std::size_t i = 1; i < TEST_N_LAYERS; ++i)
{
const std::size_t width = i % 2 == 0 ? selene_chunk_width : helios_chunk_width;
leaves_needed_for_n_layers *= width;
}
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
auto curve_trees = CurveTreesV1(
helios,
selene,
helios_chunk_width,
selene_chunk_width);
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
unit_test::BlockchainLMDBTest test_db;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
// Increment to test for off-by-1
++leaves_needed_for_n_layers;
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
// First initialize the tree with init_leaves
for (std::size_t init_leaves = 1; init_leaves < leaves_needed_for_n_layers; ++init_leaves)
{
2024-06-07 02:47:29 -04:00
// TODO: init tree once, then extend a copy of that tree
Fix grow_tree, restructure it, and clean the approach The initial impl didn't capture the following edge case: - Tree has 3 (or more) layers + 1 leaf layeri - Leaf layer last chunk IS full - Layer 0 last chunk is NOT full - Layer 1 last chunk is NOT full - Layer 2 last chunk IS NOT full In this case, when updating layer 1, we need to use layer 0's old last hash to update layer 1's old last hash. Same for Layer 2. The solution is to use logic that checks the *prev* layer when updating a layer to determine if the old last hash from the prev layer is needed. This commit restructures the grow_tree impl to account for this and simplifies the approach as follows: 1. Read the tree to get num leaf tuples + last hashes in each layer 2. Get the tree extension using the above values + new leaf tuples 2a. Prior to updating the leaf layer, call the function get_update_leaf_layer_metadata. This function uses existing totals in the leaf layer, the new total of leaf tuples, and tree params to calculate how the layer after the leaf layer should be updated. 2b. For each subsequent layer, call the function get_update_layer_metadata. This function uses the existing totals in the *prev* layer, the new total of children in the *prev* layer, and tree params to calculate how the layer should be updated. 3. Grow the tree using the tree extension. This approach isolates update logic and actual hashing into neat structured functions, rather than mix the two. This makes the code easier to follow without needing to keep so much in your head at one time.
2024-06-28 14:00:10 -04:00
// Then extend the tree with ext_leaves
for (std::size_t ext_leaves = 1; (init_leaves + ext_leaves) <= leaves_needed_for_n_layers; ++ext_leaves)
{
ASSERT_TRUE(grow_tree_in_memory(init_leaves, ext_leaves, curve_trees));
ASSERT_TRUE(grow_tree_db(init_leaves, ext_leaves, curve_trees, test_db));
}
}
}
2024-06-03 20:15:02 -04:00
//----------------------------------------------------------------------------------------------------------------------
TEST(curve_trees, trim_tree)
{
2024-07-08 23:01:59 -04:00
// TODO: consolidate code from grow_tree test
2024-06-03 20:15:02 -04:00
Helios helios;
Selene selene;
2024-07-08 23:01:59 -04:00
// Use lower values for chunk width than prod so that we can quickly test a many-layer deep tree
static const std::size_t helios_chunk_width = 3;
static const std::size_t selene_chunk_width = 3;
2024-06-03 20:15:02 -04:00
2024-07-08 23:01:59 -04:00
static_assert(helios_chunk_width > 1, "helios width must be > 1");
static_assert(selene_chunk_width > 1, "selene width must be > 1");
2024-06-03 20:15:02 -04:00
2024-07-08 23:01:59 -04:00
LOG_PRINT_L1("Test trim tree with helios chunk width " << helios_chunk_width
<< ", selene chunk width " << selene_chunk_width);
2024-06-03 20:15:02 -04:00
2024-07-08 23:01:59 -04:00
// Constant for how deep we want the tree
static const std::size_t TEST_N_LAYERS = 4;
2024-06-03 20:15:02 -04:00
// Number of leaves for which x number of layers is required
2024-07-08 23:01:59 -04:00
std::size_t leaves_needed_for_n_layers = selene_chunk_width;
for (std::size_t i = 1; i < TEST_N_LAYERS; ++i)
2024-06-03 20:15:02 -04:00
{
2024-07-08 23:01:59 -04:00
const std::size_t width = i % 2 == 0 ? selene_chunk_width : helios_chunk_width;
leaves_needed_for_n_layers *= width;
}
2024-06-07 02:47:29 -04:00
2024-07-08 23:01:59 -04:00
auto curve_trees = CurveTreesV1(
helios,
selene,
helios_chunk_width,
selene_chunk_width);
unit_test::BlockchainLMDBTest test_db;
2024-07-08 23:01:59 -04:00
// Increment to test for off-by-1
++leaves_needed_for_n_layers;
2024-06-07 02:47:29 -04:00
2024-07-08 23:01:59 -04:00
// First initialize the tree with init_leaves
for (std::size_t init_leaves = 2; init_leaves <= leaves_needed_for_n_layers; ++init_leaves)
{
2024-06-07 02:47:29 -04:00
LOG_PRINT_L1("Initializing tree with " << init_leaves << " leaves in memory");
2024-07-08 23:01:59 -04:00
CurveTreesGlobalTree global_tree(curve_trees);
2024-06-07 02:47:29 -04:00
ASSERT_TRUE(grow_tree(curve_trees, global_tree, init_leaves));
2024-07-08 23:01:59 -04:00
// Then extend the tree with ext_leaves
for (std::size_t trim_leaves = 1; trim_leaves < leaves_needed_for_n_layers; ++trim_leaves)
2024-06-03 20:15:02 -04:00
{
2024-06-07 02:47:29 -04:00
if (trim_leaves >= init_leaves)
2024-06-03 20:15:02 -04:00
continue;
2024-07-08 23:01:59 -04:00
// Copy the already existing global tree
2024-06-07 02:47:29 -04:00
CurveTreesGlobalTree tree_copy(global_tree);
2024-07-08 23:01:59 -04:00
ASSERT_TRUE(trim_tree_in_memory(trim_leaves, std::move(tree_copy)));
ASSERT_TRUE(trim_tree_db(init_leaves, trim_leaves, curve_trees, test_db));
2024-06-03 20:15:02 -04:00
}
}
}
//----------------------------------------------------------------------------------------------------------------------
// Make sure the result of hash_trim is the same as the equivalent hash_grow excluding the trimmed children
TEST(curve_trees, hash_trim)
{
2024-07-08 23:01:59 -04:00
// 1. Trim 1
{
// Start by hashing: {selene_scalar_0, selene_scalar_1}
// Then trim to: {selene_scalar_0}
const auto selene_scalar_0 = generate_random_selene_scalar();
const auto selene_scalar_1 = generate_random_selene_scalar();
// Get the initial hash of the 2 scalars
std::vector<Selene::Scalar> init_children{selene_scalar_0, selene_scalar_1};
const auto init_hash = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{init_children.data(), init_children.size()});
// Trim selene_scalar_1
const auto &trimmed_children = Selene::Chunk{init_children.data() + 1, 1};
const auto trim_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_trim(
2024-07-08 23:01:59 -04:00
init_hash,
1,
trimmed_children,
fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar());
const auto trim_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(trim_res);
2024-07-08 23:01:59 -04:00
// Now compare to calling hash_grow{selene_scalar_0}
std::vector<Selene::Scalar> remaining_children{selene_scalar_0};
const auto grow_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{remaining_children.data(), remaining_children.size()});
const auto grow_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(grow_res);
2024-07-08 23:01:59 -04:00
ASSERT_EQ(trim_res_bytes, grow_res_bytes);
}
// 2. Trim 2
2024-07-08 23:01:59 -04:00
{
// Start by hashing: {selene_scalar_0, selene_scalar_1, selene_scalar_2}
// Then trim to: {selene_scalar_0}
const auto selene_scalar_0 = generate_random_selene_scalar();
const auto selene_scalar_1 = generate_random_selene_scalar();
const auto selene_scalar_2 = generate_random_selene_scalar();
// Get the initial hash of the 3 selene scalars
std::vector<Selene::Scalar> init_children{selene_scalar_0, selene_scalar_1, selene_scalar_2};
const auto init_hash = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{init_children.data(), init_children.size()});
// Trim the initial result by 2 children
const auto &trimmed_children = Selene::Chunk{init_children.data() + 1, 2};
const auto trim_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_trim(
2024-07-08 23:01:59 -04:00
init_hash,
1,
trimmed_children,
fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar());
const auto trim_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(trim_res);
2024-07-08 23:01:59 -04:00
// Now compare to calling hash_grow{selene_scalar_0}
std::vector<Selene::Scalar> remaining_children{selene_scalar_0};
const auto grow_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{remaining_children.data(), remaining_children.size()});
const auto grow_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(grow_res);
2024-07-08 23:01:59 -04:00
ASSERT_EQ(trim_res_bytes, grow_res_bytes);
}
// 3. Change 1
{
// Start by hashing: {selene_scalar_0, selene_scalar_1}
// Then change to: {selene_scalar_0, selene_scalar_2}
const auto selene_scalar_0 = generate_random_selene_scalar();
const auto selene_scalar_1 = generate_random_selene_scalar();
// Get the initial hash of the 2 selene scalars
2024-07-08 23:01:59 -04:00
std::vector<Selene::Scalar> init_children{selene_scalar_0, selene_scalar_1};
const auto init_hash = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{init_children.data(), init_children.size()});
const auto selene_scalar_2 = generate_random_selene_scalar();
// Trim the 2nd child and grow with new child
const auto &trimmed_children = Selene::Chunk{init_children.data() + 1, 1};
const auto trim_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_trim(
2024-07-08 23:01:59 -04:00
init_hash,
1,
trimmed_children,
selene_scalar_2);
const auto trim_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(trim_res);
2024-07-08 23:01:59 -04:00
// Now compare to calling hash_grow{selene_scalar_0, selene_scalar_2}
std::vector<Selene::Scalar> remaining_children{selene_scalar_0, selene_scalar_2};
const auto grow_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{remaining_children.data(), remaining_children.size()});
const auto grow_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(grow_res);
2024-07-08 23:01:59 -04:00
ASSERT_EQ(trim_res_bytes, grow_res_bytes);
}
// 4. Trim 2 and grow back by 1
2024-07-08 23:01:59 -04:00
{
// Start by hashing: {selene_scalar_0, selene_scalar_1, selene_scalar_2}
// Then trim+grow to: {selene_scalar_0, selene_scalar_3}
const auto selene_scalar_0 = generate_random_selene_scalar();
const auto selene_scalar_1 = generate_random_selene_scalar();
const auto selene_scalar_2 = generate_random_selene_scalar();
// Get the initial hash of the 3 selene scalars
std::vector<Selene::Scalar> init_children{selene_scalar_0, selene_scalar_1, selene_scalar_2};
const auto init_hash = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{init_children.data(), init_children.size()});
const auto selene_scalar_3 = generate_random_selene_scalar();
// Trim the initial result by 2 children+grow by 1
const auto &trimmed_children = Selene::Chunk{init_children.data() + 1, 2};
const auto trim_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_trim(
2024-07-08 23:01:59 -04:00
init_hash,
1,
trimmed_children,
selene_scalar_3);
const auto trim_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(trim_res);
2024-07-08 23:01:59 -04:00
// Now compare to calling hash_grow{selene_scalar_0, selene_scalar_3}
std::vector<Selene::Scalar> remaining_children{selene_scalar_0, selene_scalar_3};
const auto grow_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
2024-07-08 23:01:59 -04:00
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
2024-07-08 23:01:59 -04:00
/*children*/ Selene::Chunk{remaining_children.data(), remaining_children.size()});
const auto grow_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(grow_res);
2024-07-08 23:01:59 -04:00
ASSERT_EQ(trim_res_bytes, grow_res_bytes);
}
}
//----------------------------------------------------------------------------------------------------------------------
TEST(curve_trees, hash_grow)
{
// Start by hashing: {selene_scalar_0, selene_scalar_1}
// Then grow 1: {selene_scalar_0, selene_scalar_1, selene_scalar_2}
// Then grow 1: {selene_scalar_0, selene_scalar_1, selene_scalar_2, selene_scalar_3}
const auto selene_scalar_0 = generate_random_selene_scalar();
const auto selene_scalar_1 = generate_random_selene_scalar();
// Get the initial hash of the 2 selene scalars
std::vector<Selene::Scalar> all_children{selene_scalar_0, selene_scalar_1};
const auto init_hash = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
/*children*/ Selene::Chunk{all_children.data(), all_children.size()});
// Extend with a new child
const auto selene_scalar_2 = generate_random_selene_scalar();
std::vector<Selene::Scalar> new_children{selene_scalar_2};
const auto ext_hash = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
init_hash,
all_children.size(),
fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
Selene::Chunk{new_children.data(), new_children.size()});
const auto ext_hash_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(ext_hash);
// Now compare to calling hash_grow{selene_scalar_0, selene_scalar_1, selene_scalar_2}
all_children.push_back(selene_scalar_2);
const auto grow_res = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
/*children*/ Selene::Chunk{all_children.data(), all_children.size()});
const auto grow_res_bytes = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(grow_res);
ASSERT_EQ(ext_hash_bytes, grow_res_bytes);
// Extend again with a new child
const auto selene_scalar_3 = generate_random_selene_scalar();
new_children.clear();
new_children = {selene_scalar_3};
const auto ext_hash2 = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
ext_hash,
all_children.size(),
fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
Selene::Chunk{new_children.data(), new_children.size()});
const auto ext_hash_bytes2 = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(ext_hash2);
// Now compare to calling hash_grow{selene_scalar_0, selene_scalar_1, selene_scalar_2, selene_scalar_3}
all_children.push_back(selene_scalar_3);
const auto grow_res2 = fcmp::curve_trees::CURVE_TREES_V1.m_c2.hash_grow(
/*existing_hash*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.m_hash_init_point,
/*offset*/ 0,
/*existing_child_at_offset*/ fcmp::curve_trees::CURVE_TREES_V1.m_c2.zero_scalar(),
/*children*/ Selene::Chunk{all_children.data(), all_children.size()});
const auto grow_res_bytes2 = fcmp::curve_trees::CURVE_TREES_V1.m_c2.to_bytes(grow_res2);
ASSERT_EQ(ext_hash_bytes2, grow_res_bytes2);
}