From 3fb460f4faad365074af5d9067faf406053d74a0 Mon Sep 17 00:00:00 2001 From: Jacob Trombetta Date: Wed, 2 Oct 2024 14:36:28 -0400 Subject: [PATCH] feat: add helper methods to dynamic Dory structure (PROOF-919) (#198) # Rationale for this change The GPU implementation of the dynamic Dory commitment computation requires a number of helper methods in the dynamic Dory structure. This PR implements methods that will be used by the GPU implementation. # What changes are included in this PR? - Add `index_from_row_and_column` method which is the inverse of `row_and_column_from_index` method. Given a `row` and `column` it will provide the `index` of the original data. The method also handles cases where `row` and `column` do not provide a valid `index`. - Add `matrix_size` method which returns the width and height of the dynamic Dory matrix structure that will hold the given data. - Made helper methods `const` for compile-time evaluation. - Add temporary `#[allow(dead_code)]` to address `cargo clippy` warnings - these will be removed when the dynamic Dory commit computation is implemented for the GPU. # Are these changes tested? Yes --- .../dory/dynamic_dory_structure.rs | 162 +++++++++++++++++- 1 file changed, 159 insertions(+), 3 deletions(-) diff --git a/crates/proof-of-sql/src/proof_primitive/dory/dynamic_dory_structure.rs b/crates/proof-of-sql/src/proof_primitive/dory/dynamic_dory_structure.rs index 37ec08bcf..b62f0a51b 100644 --- a/crates/proof-of-sql/src/proof_primitive/dory/dynamic_dory_structure.rs +++ b/crates/proof-of-sql/src/proof_primitive/dory/dynamic_dory_structure.rs @@ -30,22 +30,53 @@ //! ... //! ``` +/// Returns the full width of a row in the matrix. +/// +/// Note: when row = 1, this correctly returns 2, even though no data belongs at position 0. +#[allow(dead_code)] +pub(crate) const fn full_width_of_row(row: usize) -> usize { + ((2 * row + 4) / 3).next_power_of_two() +} + /// Returns the index that belongs in the first column in a particular row. /// /// Note: when row = 1, this correctly returns 0, even though no data belongs there. #[cfg(test)] -pub(crate) fn row_start_index(row: usize) -> usize { - let width_of_row = ((2 * row + 4) / 3).next_power_of_two(); +pub(crate) const fn row_start_index(row: usize) -> usize { + let width_of_row = full_width_of_row(row); width_of_row * (row - width_of_row / 2) } + /// Returns the (row, column) in the matrix where the data with the given index belongs. -pub(crate) fn row_and_column_from_index(index: usize) -> (usize, usize) { +pub(crate) const fn row_and_column_from_index(index: usize) -> (usize, usize) { let width_of_row = 1 << (((2 * index + 1).ilog2() + 1) / 2); let row = index / width_of_row + width_of_row / 2; let column = index % width_of_row; (row, column) } +/// Returns the index of data where the (row, column) belongs. +#[allow(dead_code)] +pub(crate) fn index_from_row_and_column(row: usize, column: usize) -> Option { + let width_of_row = full_width_of_row(row); + (column < width_of_row && (row, column) != (1, 0)) + .then_some(width_of_row * (row - width_of_row / 2) + column) +} + +/// Returns a matrix size, (height, width), that can hold the given number of data points being committed with respect to an offset. +/// +/// Note: when data_len = 0 and offset = 0, this function returns an empty matrix with size (0, 0). +#[allow(dead_code)] +pub(crate) const fn matrix_size(data_len: usize, offset: usize) -> (usize, usize) { + if data_len == 0 && offset == 0 { + return (0, 0); + } + + let (last_row, _) = row_and_column_from_index(offset + data_len - 1); + let width_of_last_row = full_width_of_row(last_row); + (last_row + 1, width_of_last_row) +} + #[cfg(test)] mod tests { use super::*; @@ -125,4 +156,129 @@ mod tests { } } } + #[test] + fn we_can_find_the_full_width_of_row() { + // This corresponds to a matrix with 2^(N+1) rows. + let N = 20; + let mut expected_widths = Vec::new(); + + // First three rows are defined by the dynamic Dory structure. + expected_widths.extend(std::iter::repeat(1).take(1)); + expected_widths.extend(std::iter::repeat(2).take(2)); + + // The rest of the rows are defined by the pattern 3*2^n rows of length 4*2^n. + for n in 0..N { + let repeat_count = 3 * 2_usize.pow(n); + let value = 4 * 2_usize.pow(n); + expected_widths.extend(std::iter::repeat(value).take(repeat_count)); + } + + // Verify the widths. + for (row, width) in expected_widths.iter().enumerate() { + let width_of_row = full_width_of_row(row); + assert_eq!( + width_of_row, *width, + "row: {} does not produce expected width", + row + ); + } + } + #[test] + fn we_can_produce_the_correct_matrix_size() { + // NOTE: returned tuple is (height, width). + assert_eq!(matrix_size(0, 0), (0, 0)); + assert_eq!(matrix_size(1, 0), (1, 1)); + assert_eq!(matrix_size(2, 0), (2, 2)); + assert_eq!(matrix_size(3, 0), (3, 2)); + assert_eq!(matrix_size(4, 0), (3, 2)); + assert_eq!(matrix_size(5, 0), (4, 4)); + assert_eq!(matrix_size(6, 0), (4, 4)); + assert_eq!(matrix_size(7, 0), (4, 4)); + assert_eq!(matrix_size(8, 0), (4, 4)); + assert_eq!(matrix_size(9, 0), (5, 4)); + assert_eq!(matrix_size(10, 0), (5, 4)); + assert_eq!(matrix_size(11, 0), (5, 4)); + assert_eq!(matrix_size(12, 0), (5, 4)); + assert_eq!(matrix_size(13, 0), (6, 4)); + assert_eq!(matrix_size(14, 0), (6, 4)); + assert_eq!(matrix_size(15, 0), (6, 4)); + assert_eq!(matrix_size(16, 0), (6, 4)); + assert_eq!(matrix_size(17, 0), (7, 8)); + + assert_eq!(matrix_size(64, 0), (12, 8)); + assert_eq!(matrix_size(71, 0), (13, 16)); + assert_eq!(matrix_size(81, 0), (14, 16)); + assert_eq!(matrix_size(98, 0), (15, 16)); + assert_eq!(matrix_size(115, 0), (16, 16)); + } + #[test] + fn we_can_produce_the_correct_matrix_size_with_offset() { + // NOTE: returned tuple is (height, width). + assert_eq!(matrix_size(0, 0), (0, 0)); + assert_eq!(matrix_size(0, 1), (1, 1)); + assert_eq!(matrix_size(0, 2), (2, 2)); + assert_eq!(matrix_size(1, 1), (2, 2)); + assert_eq!(matrix_size(1, 2), (3, 2)); + assert_eq!(matrix_size(1, 3), (3, 2)); + assert_eq!(matrix_size(1, 4), (4, 4)); + assert_eq!(matrix_size(1, 5), (4, 4)); + assert_eq!(matrix_size(1, 6), (4, 4)); + assert_eq!(matrix_size(1, 7), (4, 4)); + assert_eq!(matrix_size(1, 8), (5, 4)); + assert_eq!(matrix_size(1, 9), (5, 4)); + assert_eq!(matrix_size(1, 10), (5, 4)); + assert_eq!(matrix_size(1, 11), (5, 4)); + assert_eq!(matrix_size(1, 12), (6, 4)); + assert_eq!(matrix_size(1, 13), (6, 4)); + assert_eq!(matrix_size(1, 14), (6, 4)); + assert_eq!(matrix_size(1, 15), (6, 4)); + assert_eq!(matrix_size(1, 16), (7, 8)); + + assert_eq!(matrix_size(1, 63), (12, 8)); + assert_eq!(matrix_size(1, 70), (13, 16)); + assert_eq!(matrix_size(1, 80), (14, 16)); + assert_eq!(matrix_size(1, 97), (15, 16)); + assert_eq!(matrix_size(1, 114), (16, 16)); + } + #[test] + fn we_can_find_the_index_for_row_column_pairs() { + use std::collections::HashSet; + + const MAX_INDEX: usize = 1 << 16; + let mut valid_pairs = HashSet::new(); + + // Collect all valid (row, column) pairs + for i in 0..MAX_INDEX { + let (row, column) = row_and_column_from_index(i); + valid_pairs.insert((row, column)); + } + + let (max_row, max_column) = valid_pairs + .iter() + .fold((0, 0), |(max_row, max_column), &(row, column)| { + (max_row.max(row), max_column.max(column)) + }); + + // Check that all valid pairs are valid and all invalid pairs are invalid + for row in 0..max_row { + for column in 0..max_column { + let index = index_from_row_and_column(row, column); + if valid_pairs.contains(&(row, column)) { + assert!( + index.is_some(), + "Valid pair ({}, {}) generated no index", + row, + column + ); + } else { + assert!( + index.is_none(), + "Invalid pair ({}, {}) generated a valid index", + row, + column + ); + } + } + } + } }