From cc5f045e493db77467313a37c0b523b9d6fe4aef Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 22 Feb 2023 13:47:22 +0100 Subject: [PATCH 01/49] add new tables: Cascade & Lookup --- triton-vm/src/table.rs | 2 + triton-vm/src/table/cascade_table.rs | 65 ++++++ triton-vm/src/table/challenges.rs | 35 ++- triton-vm/src/table/constraints.rs | 2 + .../constraints/cascade_table_constraints.rs | 10 + .../constraints/lookup_table_constraints.rs | 10 + triton-vm/src/table/lookup_table.rs | 87 ++++++++ triton-vm/src/table/table_column.rs | 200 ++++++++++++++++++ triton-vm/src/vm.rs | 4 + 9 files changed, 412 insertions(+), 3 deletions(-) create mode 100644 triton-vm/src/table/cascade_table.rs create mode 100644 triton-vm/src/table/constraints/cascade_table_constraints.rs create mode 100644 triton-vm/src/table/constraints/lookup_table_constraints.rs create mode 100644 triton-vm/src/table/lookup_table.rs diff --git a/triton-vm/src/table.rs b/triton-vm/src/table.rs index 28173f397..9226ab5c1 100644 --- a/triton-vm/src/table.rs +++ b/triton-vm/src/table.rs @@ -1,3 +1,4 @@ +pub mod cascade_table; pub mod challenges; pub mod constraint_circuit; pub mod constraints; @@ -5,6 +6,7 @@ pub mod cross_table_argument; pub mod extension_table; pub mod hash_table; pub mod jump_stack_table; +pub mod lookup_table; pub mod master_table; pub mod op_stack_table; pub mod processor_table; diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs new file mode 100644 index 000000000..f3baf1369 --- /dev/null +++ b/triton-vm/src/table/cascade_table.rs @@ -0,0 +1,65 @@ +use ndarray::s; +use ndarray::ArrayView2; +use ndarray::ArrayViewMut2; +use strum::EnumCount; +use twenty_first::shared_math::b_field_element::BFieldElement; +use twenty_first::shared_math::b_field_element::BFIELD_ONE; +use twenty_first::shared_math::x_field_element::XFieldElement; + +use crate::table::challenges::Challenges; +use crate::table::constraint_circuit::ConstraintCircuit; +use crate::table::constraint_circuit::DualRowIndicator; +use crate::table::constraint_circuit::SingleRowIndicator; +use crate::table::table_column::CascadeBaseTableColumn; +use crate::table::table_column::CascadeExtTableColumn; +use crate::vm::AlgebraicExecutionTrace; + +pub const BASE_WIDTH: usize = CascadeBaseTableColumn::COUNT; +pub const EXT_WIDTH: usize = CascadeExtTableColumn::COUNT; +pub const FULL_WIDTH: usize = BASE_WIDTH + EXT_WIDTH; + +pub struct CascadeTable {} + +pub struct ExtCascadeTable {} + +impl CascadeTable { + pub fn fill_trace( + cascade_table: &mut ArrayViewMut2, + aet: &AlgebraicExecutionTrace, + ) { + } + + pub fn pad_trace( + cascade_table: &mut ArrayViewMut2, + cascade_table_length: usize, + ) { + } + + pub fn extend( + base_table: ArrayView2, + mut ext_table: ArrayViewMut2, + challenges: &Challenges, + ) { + assert_eq!(BASE_WIDTH, base_table.ncols()); + assert_eq!(EXT_WIDTH, ext_table.ncols()); + assert_eq!(base_table.nrows(), ext_table.nrows()); + } +} + +impl ExtCascadeTable { + pub fn ext_initial_constraints_as_circuits() -> Vec> { + vec![] + } + + pub fn ext_consistency_constraints_as_circuits() -> Vec> { + vec![] + } + + pub fn ext_transition_constraints_as_circuits() -> Vec> { + vec![] + } + + pub fn ext_terminal_constraints_as_circuits() -> Vec> { + vec![] + } +} diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index f67af6c9c..4b35b8c26 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -27,6 +27,7 @@ use strum_macros::EnumCount as EnumCountMacro; use strum_macros::EnumIter; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::other::random_elements; +use twenty_first::shared_math::tip5::LOOKUP_TABLE; use twenty_first::shared_math::x_field_element::XFieldElement; use crate::table::challenges::ChallengeId::*; @@ -122,6 +123,10 @@ pub enum ChallengeId { HashStateWeight14, HashStateWeight15, + /// The indeterminate for the public Evaluation Argument establishing correctness of the + /// Lookup Table. + LookupTablePublicIndeterminate, + U32LhsWeight, U32RhsWeight, U32CiWeight, @@ -144,6 +149,10 @@ pub enum ChallengeId { /// The terminal for the Evaluation Argument with standard output. StandardOutputTerminal, + + /// The terminal for the Evaluation Argument establishing correctness of the + /// [Lookup Table](crate::table::lookup_table). + LookupTablePublicTerminal, } impl ChallengeId { @@ -166,10 +175,18 @@ impl Challenges { } /// The number of weights to sample using the Fiat-Shamir heuristic. This number is lower - /// than the number of challenges: the input terminal and output terminal are not sampled but - /// computed from the public input and output. + /// than the number of challenges because several challenges are not sampled, but computed + /// from publicly known values and other, sampled challenges. + /// + /// Concretely: + /// - The [`StandardInputTerminal`] is computed from Triton VM's public input and the sampled + /// indeterminate [`StandardInputIndeterminate`]. + /// - The [`StandardOutputTerminal`] is computed from Triton VM's public output and the sampled + /// indeterminate [`StandardOutputIndeterminate`]. + /// - The [`LookupTablePublicTerminal`] is computed from the publicly known and constant + /// lookup table and the sampled indeterminate [`LookupTablePublicIndeterminate`]. pub const fn num_challenges_to_sample() -> usize { - Self::count() - 2 + Self::count() - 3 } pub fn new( @@ -186,9 +203,15 @@ impl Challenges { // the indeterminates. assert!(StandardInputIndeterminate.index() < StandardInputTerminal.index()); assert!(StandardInputIndeterminate.index() < StandardOutputTerminal.index()); + assert!(StandardInputIndeterminate.index() < LookupTablePublicTerminal.index()); assert!(StandardOutputIndeterminate.index() < StandardInputTerminal.index()); assert!(StandardOutputIndeterminate.index() < StandardOutputTerminal.index()); + assert!(StandardOutputIndeterminate.index() < LookupTablePublicTerminal.index()); + + assert!(LookupTablePublicIndeterminate.index() < StandardInputTerminal.index()); + assert!(LookupTablePublicIndeterminate.index() < StandardOutputTerminal.index()); + assert!(LookupTablePublicIndeterminate.index() < LookupTablePublicTerminal.index()); let input_terminal = EvalArg::compute_terminal( public_input, @@ -200,9 +223,15 @@ impl Challenges { EvalArg::default_initial(), challenges[StandardOutputIndeterminate.index()], ); + let lookup_terminal = EvalArg::compute_terminal( + &LOOKUP_TABLE.map(|i| BFieldElement::new(i as u64)), + EvalArg::default_initial(), + challenges[LookupTablePublicIndeterminate.index()], + ); challenges.insert(StandardInputTerminal.index(), input_terminal); challenges.insert(StandardOutputTerminal.index(), output_terminal); + challenges.insert(LookupTablePublicTerminal.index(), lookup_terminal); assert_eq!(challenges.len(), Self::count()); let challenges = challenges.try_into().unwrap(); diff --git a/triton-vm/src/table/constraints.rs b/triton-vm/src/table/constraints.rs index 5e1397686..a1987dc85 100644 --- a/triton-vm/src/table/constraints.rs +++ b/triton-vm/src/table/constraints.rs @@ -1,5 +1,7 @@ +pub mod cascade_table_constraints; pub mod hash_table_constraints; pub mod jump_stack_table_constraints; +pub mod lookup_table_constraints; pub mod op_stack_table_constraints; pub mod processor_table_constraints; pub mod program_table_constraints; diff --git a/triton-vm/src/table/constraints/cascade_table_constraints.rs b/triton-vm/src/table/constraints/cascade_table_constraints.rs new file mode 100644 index 000000000..312c9a89d --- /dev/null +++ b/triton-vm/src/table/constraints/cascade_table_constraints.rs @@ -0,0 +1,10 @@ +use crate::table::cascade_table::ExtCascadeTable; +use crate::table::extension_table::Evaluable; +use crate::table::extension_table::Quotientable; + +// This file is a placeholder for auto-generated code +// Run `cargo run --bin constraint-evaluation-generator` +// to fill in this file with optimized constraints. +impl Evaluable for ExtCascadeTable {} + +impl Quotientable for ExtCascadeTable {} diff --git a/triton-vm/src/table/constraints/lookup_table_constraints.rs b/triton-vm/src/table/constraints/lookup_table_constraints.rs new file mode 100644 index 000000000..75817202d --- /dev/null +++ b/triton-vm/src/table/constraints/lookup_table_constraints.rs @@ -0,0 +1,10 @@ +use crate::table::extension_table::Evaluable; +use crate::table::extension_table::Quotientable; +use crate::table::lookup_table::ExtLookupTable; + +// This file is a placeholder for auto-generated code +// Run `cargo run --bin constraint-evaluation-generator` +// to fill in this file with optimized constraints. +impl Evaluable for ExtLookupTable {} + +impl Quotientable for ExtLookupTable {} diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs new file mode 100644 index 000000000..e6ec14366 --- /dev/null +++ b/triton-vm/src/table/lookup_table.rs @@ -0,0 +1,87 @@ +use ndarray::s; +use ndarray::ArrayView2; +use ndarray::ArrayViewMut2; +use strum::EnumCount; +use twenty_first::shared_math::b_field_element::BFieldElement; +use twenty_first::shared_math::b_field_element::BFIELD_ONE; +use twenty_first::shared_math::b_field_element::BFIELD_ZERO; +use twenty_first::shared_math::tip5::Tip5; +use twenty_first::shared_math::x_field_element::XFieldElement; + +use crate::table::challenges::Challenges; +use crate::table::constraint_circuit::ConstraintCircuit; +use crate::table::constraint_circuit::DualRowIndicator; +use crate::table::constraint_circuit::SingleRowIndicator; +use crate::table::table_column::LookupBaseTableColumn; +use crate::table::table_column::LookupBaseTableColumn::*; +use crate::table::table_column::LookupExtTableColumn; +use crate::table::table_column::LookupExtTableColumn::*; +use crate::vm::AlgebraicExecutionTrace; + +pub const BASE_WIDTH: usize = LookupBaseTableColumn::COUNT; +pub const EXT_WIDTH: usize = LookupExtTableColumn::COUNT; +pub const FULL_WIDTH: usize = BASE_WIDTH + EXT_WIDTH; + +/// The lookup table for [Tip5](https://eprint.iacr.org/2023/107.pdf)'s split-and-lookup S-Box, +/// which uses the Fermat cube map over the finite field with 2^8 + 1 elements. +pub const LOOKUP_TABLE: [BFieldElement; 1 << 8] = lookup_table(); + +/// Compute the lookup table for the Fermat cube map. Helper method for the [`LOOKUP_TABLE`], +/// which should be used instead. +const fn lookup_table() -> [BFieldElement; 1 << 8] { + let mut lookup_table = [BFIELD_ZERO; 1 << 8]; + let mut i = 0; + while i < (1 << 8) { + lookup_table[i] = BFieldElement::new(Tip5::LOOKUP_TABLE[i] as u64); + i += 1; + } + lookup_table +} + +pub struct LookupTable {} + +pub struct ExtLookupTable {} + +impl LookupTable { + pub fn fill_trace( + lookup_table: &mut ArrayViewMut2, + aet: &AlgebraicExecutionTrace, + ) { + } + + pub fn pad_trace(lookup_table: &mut ArrayViewMut2) { + // The Lookup Table is always fully populated. + let lookup_table_length: usize = 1 << 8; + lookup_table + .slice_mut(s![lookup_table_length.., IsPadding.base_table_index()]) + .fill(BFIELD_ONE); + } + + pub fn extend( + base_table: ArrayView2, + mut ext_table: ArrayViewMut2, + challenges: &Challenges, + ) { + assert_eq!(BASE_WIDTH, base_table.ncols()); + assert_eq!(EXT_WIDTH, ext_table.ncols()); + assert_eq!(base_table.nrows(), ext_table.nrows()); + } +} + +impl ExtLookupTable { + pub fn ext_initial_constraints_as_circuits() -> Vec> { + vec![] + } + + pub fn ext_consistency_constraints_as_circuits() -> Vec> { + vec![] + } + + pub fn ext_transition_constraints_as_circuits() -> Vec> { + vec![] + } + + pub fn ext_terminal_constraints_as_circuits() -> Vec> { + vec![] + } +} diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index 70ad5e778..3c18a6125 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -7,8 +7,11 @@ use strum_macros::Display; use strum_macros::EnumCount as EnumCountMacro; use strum_macros::EnumIter; +use crate::table::master_table::CASCADE_TABLE_START; +use crate::table::master_table::EXT_CASCADE_TABLE_START; use crate::table::master_table::EXT_HASH_TABLE_START; use crate::table::master_table::EXT_JUMP_STACK_TABLE_START; +use crate::table::master_table::EXT_LOOKUP_TABLE_START; use crate::table::master_table::EXT_OP_STACK_TABLE_START; use crate::table::master_table::EXT_PROCESSOR_TABLE_START; use crate::table::master_table::EXT_PROGRAM_TABLE_START; @@ -16,6 +19,7 @@ use crate::table::master_table::EXT_RAM_TABLE_START; use crate::table::master_table::EXT_U32_TABLE_START; use crate::table::master_table::HASH_TABLE_START; use crate::table::master_table::JUMP_STACK_TABLE_START; +use crate::table::master_table::LOOKUP_TABLE_START; use crate::table::master_table::OP_STACK_TABLE_START; use crate::table::master_table::PROCESSOR_TABLE_START; use crate::table::master_table::PROGRAM_TABLE_START; @@ -249,6 +253,77 @@ pub enum HashExtTableColumn { SpongeRunningEvaluation, } +// -------- Cascade Table -------- + +#[repr(usize)] +#[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] +pub enum CascadeBaseTableColumn { + /// The more significant bits of the lookup input. + LookInHi, + + /// The less significant bits of the lookup input. + LookInLo, + + /// The more significant bits of the lookup output. + LookOutHi, + + /// The less significant bits of the lookup output. + LookOutLo, + + /// The number of times the S-Box is evaluated, _i.e._, the value is looked up. + LookupMultiplicity, +} + +#[repr(usize)] +#[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] +pub enum CascadeExtTableColumn { + /// The (running sum of the) logarithmic derivative for the Lookup Argument with the Hash Table. + /// In every row, the sum accumulates `LookupMultiplicity / Combo` where `Combo` is the + /// verifier-weighted combination of + /// - `LookInHi · 2^32 + LookInLo`, and + /// - `LookOutHi · 2^32 + LookOutLo`. + HashTableServerLogDerivative, + + /// The (running sum of the) logarithmic derivative for the Lookup Argument with the Lookup + /// Table. In every row, accumulates the two summands + /// - `1 / combo_hi` where `combo_hi` is the verifier-weighted combination of `LookInHi` and + /// `LookOutHi`, and + /// - `1 / combo_lo` where `combo_lo` is the verifier-weighted combination of `LookInLo` and + /// `LookOutLo`. + LookupTableClientLogDerivative, +} + +// -------- Lookup Table -------- + +#[repr(usize)] +#[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] +pub enum LookupBaseTableColumn { + /// Indicates whether the current row is a padding row. + IsPadding, + + /// The lookup input. + LookIn, + + /// The lookup output. + LookOut, + + /// The number of times the value is looked up. + LookupMultiplicity, +} + +#[repr(usize)] +#[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] +pub enum LookupExtTableColumn { + /// The (running sum of the) logarithmic derivative for the Lookup Argument with the Cascade + /// Table. In every row, accumulates the summand `LookupMultiplicity / Combo` where `Combo` is + /// the verifier-weighted combination of `LookIn` and `LookOut`. + CascadeTableServerLogDerivative, + + /// The running sum for the public evaluation argument of the Lookup Table. + /// In every row, accumulates the verifier-weighted combination of `LookIn` and `LookOut`. + PublicEvaluationArgument, +} + // -------- U32 Table -------- #[repr(usize)] @@ -383,6 +458,30 @@ impl MasterBaseTableColumn for HashBaseTableColumn { } } +impl MasterBaseTableColumn for CascadeBaseTableColumn { + #[inline] + fn base_table_index(&self) -> usize { + (*self) as usize + } + + #[inline] + fn master_base_table_index(&self) -> usize { + CASCADE_TABLE_START + self.base_table_index() + } +} + +impl MasterBaseTableColumn for LookupBaseTableColumn { + #[inline] + fn base_table_index(&self) -> usize { + (*self) as usize + } + + #[inline] + fn master_base_table_index(&self) -> usize { + LOOKUP_TABLE_START + self.base_table_index() + } +} + impl MasterBaseTableColumn for U32BaseTableColumn { #[inline] fn base_table_index(&self) -> usize { @@ -483,6 +582,30 @@ impl MasterExtTableColumn for HashExtTableColumn { } } +impl MasterExtTableColumn for CascadeExtTableColumn { + #[inline] + fn ext_table_index(&self) -> usize { + (*self) as usize + } + + #[inline] + fn master_ext_table_index(&self) -> usize { + EXT_CASCADE_TABLE_START + self.ext_table_index() + } +} + +impl MasterExtTableColumn for LookupExtTableColumn { + #[inline] + fn ext_table_index(&self) -> usize { + (*self) as usize + } + + #[inline] + fn master_ext_table_index(&self) -> usize { + EXT_LOOKUP_TABLE_START + self.ext_table_index() + } +} + impl MasterExtTableColumn for U32ExtTableColumn { #[inline] fn ext_table_index(&self) -> usize { @@ -501,12 +624,15 @@ impl MasterExtTableColumn for U32ExtTableColumn { mod table_column_tests { use strum::IntoEnumIterator; + use crate::table::cascade_table; use crate::table::hash_table; use crate::table::jump_stack_table; + use crate::table::lookup_table; use crate::table::op_stack_table; use crate::table::processor_table; use crate::table::program_table; use crate::table::ram_table; + use crate::table::u32_table; use super::*; @@ -566,6 +692,33 @@ mod table_column_tests { + 1, "HashTable's BASE_WIDTH is 1 + its max column index", ); + assert_eq!( + cascade_table::BASE_WIDTH, + CascadeBaseTableColumn::iter() + .last() + .unwrap() + .base_table_index() + + 1, + "CascadeTable's BASE_WIDTH is 1 + its max column index", + ); + assert_eq!( + lookup_table::BASE_WIDTH, + LookupBaseTableColumn::iter() + .last() + .unwrap() + .base_table_index() + + 1, + "LookupTable's BASE_WIDTH is 1 + its max column index", + ); + assert_eq!( + u32_table::BASE_WIDTH, + U32BaseTableColumn::iter() + .last() + .unwrap() + .base_table_index() + + 1, + "U32Table's BASE_WIDTH is 1 + its max column index", + ); assert_eq!( program_table::EXT_WIDTH, @@ -613,6 +766,29 @@ mod table_column_tests { HashExtTableColumn::iter().last().unwrap().ext_table_index() + 1, "HashTable's EXT_WIDTH is 1 + its max column index", ); + assert_eq!( + cascade_table::EXT_WIDTH, + CascadeExtTableColumn::iter() + .last() + .unwrap() + .ext_table_index() + + 1, + "CascadeTable's EXT_WIDTH is 1 + its max column index", + ); + assert_eq!( + lookup_table::EXT_WIDTH, + LookupExtTableColumn::iter() + .last() + .unwrap() + .ext_table_index() + + 1, + "LookupTable's EXT_WIDTH is 1 + its max column index", + ); + assert_eq!( + u32_table::EXT_WIDTH, + U32ExtTableColumn::iter().last().unwrap().ext_table_index() + 1, + "U32Table's EXT_WIDTH is 1 + its max column index", + ); } #[test] @@ -642,6 +818,18 @@ mod table_column_tests { assert_eq!(expected_column_index, column.master_base_table_index()); expected_column_index += 1; } + for column in CascadeBaseTableColumn::iter() { + assert_eq!(expected_column_index, column.master_base_table_index()); + expected_column_index += 1; + } + for column in LookupBaseTableColumn::iter() { + assert_eq!(expected_column_index, column.master_base_table_index()); + expected_column_index += 1; + } + for column in U32BaseTableColumn::iter() { + assert_eq!(expected_column_index, column.master_base_table_index()); + expected_column_index += 1; + } } #[test] @@ -671,5 +859,17 @@ mod table_column_tests { assert_eq!(expected_column_index, column.master_ext_table_index()); expected_column_index += 1; } + for column in CascadeExtTableColumn::iter() { + assert_eq!(expected_column_index, column.master_ext_table_index()); + expected_column_index += 1; + } + for column in LookupExtTableColumn::iter() { + assert_eq!(expected_column_index, column.master_ext_table_index()); + expected_column_index += 1; + } + for column in U32ExtTableColumn::iter() { + assert_eq!(expected_column_index, column.master_ext_table_index()); + expected_column_index += 1; + } } } diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 1c9e06d43..7a3a1336b 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -948,6 +948,10 @@ pub struct AlgebraicExecutionTrace { /// alongside the u32 instruction that was executed at the time. Additionally, it records how /// often the instruction was executed with these arguments. pub u32_entries: HashMap<(Instruction, BFieldElement, BFieldElement), u64>, + + pub cascade_table_lookup_multiplicities: HashMap, + + pub lookup_table_lookup_multiplicities: [u64; 1 << 16], } impl AlgebraicExecutionTrace { From 686f0c1d1a03ef465ea753b9562ca54a1cff8e8d Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 23 Feb 2023 08:14:59 +0100 Subject: [PATCH 02/49] add new tables to constraint generator --- constraint-evaluation-generator/src/main.rs | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/constraint-evaluation-generator/src/main.rs b/constraint-evaluation-generator/src/main.rs index 24e698ced..45a6755b1 100644 --- a/constraint-evaluation-generator/src/main.rs +++ b/constraint-evaluation-generator/src/main.rs @@ -5,11 +5,13 @@ use itertools::Itertools; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::x_field_element::XFieldElement; +use triton_vm::table::cascade_table::ExtCascadeTable; use triton_vm::table::constraint_circuit::CircuitExpression; use triton_vm::table::constraint_circuit::ConstraintCircuit; use triton_vm::table::constraint_circuit::InputIndicator; use triton_vm::table::hash_table::ExtHashTable; use triton_vm::table::jump_stack_table::ExtJumpStackTable; +use triton_vm::table::lookup_table::ExtLookupTable; use triton_vm::table::op_stack_table::ExtOpStackTable; use triton_vm::table::processor_table::ExtProcessorTable; use triton_vm::table::program_table::ExtProgramTable; @@ -86,6 +88,28 @@ fn main() { ); write(&table_name_snake, source_code); + let (table_name_snake, table_name_camel) = construct_needed_table_identifiers(&["cascade"]); + let source_code = gen( + &table_name_snake, + &table_name_camel, + &mut ExtCascadeTable::ext_initial_constraints_as_circuits(), + &mut ExtCascadeTable::ext_consistency_constraints_as_circuits(), + &mut ExtCascadeTable::ext_transition_constraints_as_circuits(), + &mut ExtCascadeTable::ext_terminal_constraints_as_circuits(), + ); + write(&table_name_snake, source_code); + + let (table_name_snake, table_name_camel) = construct_needed_table_identifiers(&["lookup"]); + let source_code = gen( + &table_name_snake, + &table_name_camel, + &mut ExtLookupTable::ext_initial_constraints_as_circuits(), + &mut ExtLookupTable::ext_consistency_constraints_as_circuits(), + &mut ExtLookupTable::ext_transition_constraints_as_circuits(), + &mut ExtLookupTable::ext_terminal_constraints_as_circuits(), + ); + write(&table_name_snake, source_code); + let (table_name_snake, table_name_camel) = construct_needed_table_identifiers(&["u32"]); let source_code = gen( &table_name_snake, From 471f86eb0aa89edde5654d38288b7f0f725d4b96 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 23 Feb 2023 08:17:18 +0100 Subject: [PATCH 03/49] add functionality for recording lookup multiplicities --- triton-vm/src/vm.rs | 48 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 7a3a1336b..053fb2434 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -23,6 +23,8 @@ use twenty_first::shared_math::rescue_prime_regular::NUM_ROUNDS; use twenty_first::shared_math::rescue_prime_regular::RATE; use twenty_first::shared_math::rescue_prime_regular::ROUND_CONSTANTS; use twenty_first::shared_math::rescue_prime_regular::STATE_SIZE; +use twenty_first::shared_math::tip5; +use twenty_first::shared_math::tip5::Tip5; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; use twenty_first::util_types::algebraic_hasher::Domain; @@ -949,9 +951,11 @@ pub struct AlgebraicExecutionTrace { /// often the instruction was executed with these arguments. pub u32_entries: HashMap<(Instruction, BFieldElement, BFieldElement), u64>, - pub cascade_table_lookup_multiplicities: HashMap, + /// Records how often each entry in the cascade table was looked up. + pub cascade_table_lookup_multiplicities: HashMap, - pub lookup_table_lookup_multiplicities: [u64; 1 << 16], + /// Records how often each entry in the lookup table was looked up. + pub lookup_table_lookup_multiplicities: [u64; 1 << 8], } impl AlgebraicExecutionTrace { @@ -964,11 +968,17 @@ impl AlgebraicExecutionTrace { hash_trace: Array2::default([0, hash_table::BASE_WIDTH]), sponge_trace: Array2::default([0, hash_table::BASE_WIDTH]), u32_entries: HashMap::new(), + cascade_table_lookup_multiplicities: HashMap::new(), + lookup_table_lookup_multiplicities: [0; 1 << 8], } } - pub fn append_hash_trace(&mut self, xlix_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1]) { - let mut hash_trace_addendum = Self::add_round_number_and_constants(xlix_trace); + pub fn append_hash_trace( + &mut self, + hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + ) { + self.increase_lookup_multiplicities(hash_permutation_trace); + let mut hash_trace_addendum = Self::add_round_number_and_constants(hash_permutation_trace); hash_trace_addendum .slice_mut(s![.., CI.base_table_index()]) .fill(Instruction::Hash.opcode_b()); @@ -980,13 +990,15 @@ impl AlgebraicExecutionTrace { pub fn append_sponge_trace( &mut self, instruction: Instruction, - xlix_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], ) { assert!(matches!( instruction, Instruction::AbsorbInit | Instruction::Absorb | Instruction::Squeeze )); - let mut sponge_trace_addendum = Self::add_round_number_and_constants(xlix_trace); + self.increase_lookup_multiplicities(hash_permutation_trace); + let mut sponge_trace_addendum = + Self::add_round_number_and_constants(hash_permutation_trace); sponge_trace_addendum .slice_mut(s![.., CI.base_table_index()]) .fill(instruction.opcode_b()); @@ -995,6 +1007,30 @@ impl AlgebraicExecutionTrace { .expect("shapes must be identical"); } + /// Given a trace of the hash function's permutation, determines how often each entry in the + /// - cascade table was looked up, and + /// - lookup table was looked up + /// and increases the multiplicities accordingly + fn increase_lookup_multiplicities( + &mut self, + hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + ) { + for row in hash_permutation_trace.iter().rev().skip(1) { + for state_element in row[0..tip5::NUM_SPLIT_AND_LOOKUP].iter() { + for limb in state_element.raw_u16s() { + let limb_lo = limb & 0xff; + let limb_hi = (limb >> 8) & 0xff; + self.lookup_table_lookup_multiplicities[limb_lo as usize] += 1; + self.lookup_table_lookup_multiplicities[limb_hi as usize] += 1; + self.cascade_table_lookup_multiplicities + .entry(limb) + .and_modify(|e| *e += 1) + .or_insert(1); + } + } + } + } + /// Given an XLIX trace, this function adds /// /// 1. the round number, and From 7ce1fd03e83ff70ee50f549e86ca4d97dbd56205 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 23 Feb 2023 08:18:11 +0100 Subject: [PATCH 04/49] add new tables to master table --- triton-vm/src/table/master_table.rs | 243 +++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 6 deletions(-) diff --git a/triton-vm/src/table/master_table.rs b/triton-vm/src/table/master_table.rs index 557ded62a..a48626d66 100644 --- a/triton-vm/src/table/master_table.rs +++ b/triton-vm/src/table/master_table.rs @@ -39,6 +39,8 @@ use triton_opcodes::instruction::Instruction; use crate::arithmetic_domain::ArithmeticDomain; use crate::stark::StarkHasher; +use crate::table::cascade_table::CascadeTable; +use crate::table::cascade_table::ExtCascadeTable; use crate::table::challenges::Challenges; use crate::table::cross_table_argument::GrandCrossTableArg; use crate::table::extension_table::DegreeWithOrigin; @@ -48,6 +50,8 @@ use crate::table::hash_table::ExtHashTable; use crate::table::hash_table::HashTable; use crate::table::jump_stack_table::ExtJumpStackTable; use crate::table::jump_stack_table::JumpStackTable; +use crate::table::lookup_table::ExtLookupTable; +use crate::table::lookup_table::LookupTable; use crate::table::op_stack_table::ExtOpStackTable; use crate::table::op_stack_table::OpStackTable; use crate::table::processor_table::ExtProcessorTable; @@ -69,6 +73,8 @@ pub const NUM_BASE_COLUMNS: usize = program_table::BASE_WIDTH + ram_table::BASE_WIDTH + jump_stack_table::BASE_WIDTH + hash_table::BASE_WIDTH + + cascade_table::BASE_WIDTH + + lookup_table::BASE_WIDTH + u32_table::BASE_WIDTH; pub const NUM_EXT_COLUMNS: usize = program_table::EXT_WIDTH + processor_table::EXT_WIDTH @@ -76,6 +82,8 @@ pub const NUM_EXT_COLUMNS: usize = program_table::EXT_WIDTH + ram_table::EXT_WIDTH + jump_stack_table::EXT_WIDTH + hash_table::EXT_WIDTH + + cascade_table::EXT_WIDTH + + lookup_table::EXT_WIDTH + u32_table::EXT_WIDTH; pub const NUM_COLUMNS: usize = NUM_BASE_COLUMNS + NUM_EXT_COLUMNS; @@ -91,7 +99,11 @@ pub const JUMP_STACK_TABLE_START: usize = RAM_TABLE_END; pub const JUMP_STACK_TABLE_END: usize = JUMP_STACK_TABLE_START + jump_stack_table::BASE_WIDTH; pub const HASH_TABLE_START: usize = JUMP_STACK_TABLE_END; pub const HASH_TABLE_END: usize = HASH_TABLE_START + hash_table::BASE_WIDTH; -pub const U32_TABLE_START: usize = HASH_TABLE_END; +pub const CASCADE_TABLE_START: usize = HASH_TABLE_END; +pub const CASCADE_TABLE_END: usize = CASCADE_TABLE_START + cascade_table::BASE_WIDTH; +pub const LOOKUP_TABLE_START: usize = CASCADE_TABLE_END; +pub const LOOKUP_TABLE_END: usize = LOOKUP_TABLE_START + lookup_table::BASE_WIDTH; +pub const U32_TABLE_START: usize = LOOKUP_TABLE_END; pub const U32_TABLE_END: usize = U32_TABLE_START + u32_table::BASE_WIDTH; pub const EXT_PROGRAM_TABLE_START: usize = 0; @@ -107,7 +119,11 @@ pub const EXT_JUMP_STACK_TABLE_END: usize = EXT_JUMP_STACK_TABLE_START + jump_stack_table::EXT_WIDTH; pub const EXT_HASH_TABLE_START: usize = EXT_JUMP_STACK_TABLE_END; pub const EXT_HASH_TABLE_END: usize = EXT_HASH_TABLE_START + hash_table::EXT_WIDTH; -pub const EXT_U32_TABLE_START: usize = EXT_HASH_TABLE_END; +pub const EXT_CASCADE_TABLE_START: usize = EXT_HASH_TABLE_END; +pub const EXT_CASCADE_TABLE_END: usize = EXT_CASCADE_TABLE_START + cascade_table::EXT_WIDTH; +pub const EXT_LOOKUP_TABLE_START: usize = EXT_CASCADE_TABLE_END; +pub const EXT_LOOKUP_TABLE_END: usize = EXT_LOOKUP_TABLE_START + lookup_table::EXT_WIDTH; +pub const EXT_U32_TABLE_START: usize = EXT_LOOKUP_TABLE_END; pub const EXT_U32_TABLE_END: usize = EXT_U32_TABLE_START + u32_table::EXT_WIDTH; /// A `TableId` uniquely determines one of Triton VM's tables. @@ -119,6 +135,8 @@ pub enum TableId { RamTable, JumpStackTable, HashTable, + CascadeTable, + LookupTable, U32Table, } @@ -224,6 +242,7 @@ pub struct MasterBaseTable { pub program_len: usize, pub main_execution_len: usize, pub hash_coprocessor_execution_len: usize, + pub cascade_table_len: usize, pub u32_coprocesor_execution_len: usize, pub randomized_padded_trace_len: usize, @@ -313,6 +332,8 @@ impl MasterBaseTable { Self::program_table_length(aet) + 1, Self::processor_table_length(aet), Self::hash_table_length(aet), + Self::cascade_table_length(aet), + Self::lookup_table_length(), Self::u32_table_length(aet), ] .iter() @@ -334,6 +355,14 @@ impl MasterBaseTable { aet.sponge_trace.nrows() + aet.hash_trace.nrows() } + pub fn cascade_table_length(aet: &AlgebraicExecutionTrace) -> usize { + aet.cascade_table_lookup_multiplicities.len() + } + + pub fn lookup_table_length() -> usize { + 1 << 8 + } + pub fn u32_table_length(aet: &AlgebraicExecutionTrace) -> usize { aet.u32_entries .keys() @@ -369,6 +398,7 @@ impl MasterBaseTable { program_len: Self::program_table_length(&aet), main_execution_len: Self::processor_table_length(&aet), hash_coprocessor_execution_len: Self::hash_table_length(&aet), + cascade_table_len: Self::cascade_table_length(&aet), u32_coprocesor_execution_len: Self::u32_table_length(&aet), randomized_padded_trace_len, rand_trace_to_padded_trace_unit_distance: unit_distance, @@ -386,6 +416,10 @@ impl MasterBaseTable { let clk_jump_diffs_jump_stack = JumpStackTable::fill_trace(jump_stack_table, &aet); let hash_table = &mut master_base_table.table_mut(TableId::HashTable); HashTable::fill_trace(hash_table, &aet); + let cascade_table = &mut master_base_table.table_mut(TableId::CascadeTable); + CascadeTable::fill_trace(cascade_table, &aet); + let lookup_table = &mut master_base_table.table_mut(TableId::LookupTable); + LookupTable::fill_trace(lookup_table, &aet); let u32_table = &mut master_base_table.table_mut(TableId::U32Table); U32Table::fill_trace(u32_table, &aet); @@ -407,6 +441,7 @@ impl MasterBaseTable { let program_len = self.program_len; let main_execution_len = self.main_execution_len; let hash_coprocessor_execution_len = self.hash_coprocessor_execution_len; + let cascade_table_len = self.cascade_table_len; let u32_table_len = self.u32_coprocesor_execution_len; let program_table = &mut self.table_mut(TableId::ProgramTable); @@ -421,6 +456,10 @@ impl MasterBaseTable { JumpStackTable::pad_trace(jump_stack_table, main_execution_len); let hash_table = &mut self.table_mut(TableId::HashTable); HashTable::pad_trace(hash_table, hash_coprocessor_execution_len); + let cascade_table = &mut self.table_mut(TableId::CascadeTable); + CascadeTable::pad_trace(cascade_table, cascade_table_len); + let lookup_table = &mut self.table_mut(TableId::LookupTable); + LookupTable::pad_trace(lookup_table); let u32_table = &mut self.table_mut(TableId::U32Table); U32Table::pad_trace(u32_table, u32_table_len); } @@ -507,6 +546,16 @@ impl MasterBaseTable { master_ext_table.table_mut(TableId::HashTable), challenges, ); + CascadeTable::extend( + self.table(TableId::CascadeTable), + master_ext_table.table_mut(TableId::CascadeTable), + challenges, + ); + LookupTable::extend( + self.table(TableId::LookupTable), + master_ext_table.table_mut(TableId::LookupTable), + challenges, + ); U32Table::extend( self.table(TableId::U32Table), master_ext_table.table_mut(TableId::U32Table), @@ -525,6 +574,8 @@ impl MasterBaseTable { RamTable => (RAM_TABLE_START, RAM_TABLE_END), JumpStackTable => (JUMP_STACK_TABLE_START, JUMP_STACK_TABLE_END), HashTable => (HASH_TABLE_START, HASH_TABLE_END), + CascadeTable => (CASCADE_TABLE_START, CASCADE_TABLE_END), + LookupTable => (LOOKUP_TABLE_START, LOOKUP_TABLE_END), U32Table => (U32_TABLE_START, U32_TABLE_END), } } @@ -596,6 +647,8 @@ impl MasterExtTable { RamTable => (EXT_RAM_TABLE_START, EXT_RAM_TABLE_END), JumpStackTable => (EXT_JUMP_STACK_TABLE_START, EXT_JUMP_STACK_TABLE_END), HashTable => (EXT_HASH_TABLE_START, EXT_HASH_TABLE_END), + CascadeTable => (EXT_CASCADE_TABLE_START, EXT_CASCADE_TABLE_END), + LookupTable => (EXT_LOOKUP_TABLE_START, EXT_LOOKUP_TABLE_END), U32Table => (EXT_U32_TABLE_START, EXT_U32_TABLE_END), } } @@ -628,6 +681,8 @@ pub fn all_degrees_with_origin( ExtRamTable::all_degrees_with_origin("ram table", id, ph), ExtJumpStackTable::all_degrees_with_origin("jump stack table", id, ph), ExtHashTable::all_degrees_with_origin("hash table", id, ph), + ExtCascadeTable::all_degrees_with_origin("cascade table", id, ph), + ExtLookupTable::all_degrees_with_origin("lookup table", id, ph), ExtU32Table::all_degrees_with_origin("u32 table", id, ph), ] .concat() @@ -657,6 +712,8 @@ pub fn num_all_initial_quotients() -> usize { + ExtRamTable::num_initial_quotients() + ExtJumpStackTable::num_initial_quotients() + ExtHashTable::num_initial_quotients() + + ExtCascadeTable::num_initial_quotients() + + ExtLookupTable::num_initial_quotients() + ExtU32Table::num_initial_quotients() } @@ -667,6 +724,8 @@ pub fn num_all_consistency_quotients() -> usize { + ExtRamTable::num_consistency_quotients() + ExtJumpStackTable::num_consistency_quotients() + ExtHashTable::num_consistency_quotients() + + ExtCascadeTable::num_consistency_quotients() + + ExtLookupTable::num_consistency_quotients() + ExtU32Table::num_consistency_quotients() } @@ -677,6 +736,8 @@ pub fn num_all_transition_quotients() -> usize { + ExtRamTable::num_transition_quotients() + ExtJumpStackTable::num_transition_quotients() + ExtHashTable::num_transition_quotients() + + ExtCascadeTable::num_transition_quotients() + + ExtLookupTable::num_transition_quotients() + ExtU32Table::num_transition_quotients() } @@ -687,6 +748,8 @@ pub fn num_all_terminal_quotients() -> usize { + ExtRamTable::num_terminal_quotients() + ExtJumpStackTable::num_terminal_quotients() + ExtHashTable::num_terminal_quotients() + + ExtCascadeTable::num_terminal_quotients() + + ExtLookupTable::num_terminal_quotients() + ExtU32Table::num_terminal_quotients() + GrandCrossTableArg::num_terminal_quotients() } @@ -699,6 +762,8 @@ pub fn all_initial_quotient_degree_bounds(interpolant_degree: Degree) -> Vec Vec10} | {:>9} | {:>10} |", + "CascadeTable", + cascade_table::BASE_WIDTH, + cascade_table::EXT_WIDTH, + cascade_table::FULL_WIDTH + ); + println!( + "| {:<18} | {:>10} | {:>9} | {:>10} |", + "LookupTable", + lookup_table::BASE_WIDTH, + lookup_table::EXT_WIDTH, + lookup_table::FULL_WIDTH + ); println!( "| {:<18} | {:>10} | {:>9} | {:>10} |", "U32Table", @@ -1876,6 +2083,18 @@ mod master_table_tests { column.master_base_table_index() ); } + for column in CascadeBaseTableColumn::iter() { + println!( + "{:>3} | cascade | {column}", + column.master_base_table_index() + ); + } + for column in LookupBaseTableColumn::iter() { + println!( + "{:>3} | lookup | {column}", + column.master_base_table_index() + ); + } for column in U32BaseTableColumn::iter() { println!( "{:>3} | u32 | {column}", @@ -1921,6 +2140,18 @@ mod master_table_tests { column.master_ext_table_index() ); } + for column in CascadeExtTableColumn::iter() { + println!( + "{:>3} | cascade | {column}", + column.master_ext_table_index() + ); + } + for column in LookupExtTableColumn::iter() { + println!( + "{:>3} | lookup | {column}", + column.master_ext_table_index() + ); + } for column in U32ExtTableColumn::iter() { println!( "{:>3} | u32 | {column}", From e29b108786b3fa97936058bc16372a33e43d27db Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 24 Feb 2023 08:09:35 +0100 Subject: [PATCH 05/49] fill Lookup Table --- triton-vm/src/table/lookup_table.rs | 42 ++++++++++++++++------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index e6ec14366..0024044c7 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -1,11 +1,11 @@ use ndarray::s; +use ndarray::Array1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; -use twenty_first::shared_math::b_field_element::BFIELD_ZERO; -use twenty_first::shared_math::tip5::Tip5; +use twenty_first::shared_math::tip5; use twenty_first::shared_math::x_field_element::XFieldElement; use crate::table::challenges::Challenges; @@ -15,29 +15,13 @@ use crate::table::constraint_circuit::SingleRowIndicator; use crate::table::table_column::LookupBaseTableColumn; use crate::table::table_column::LookupBaseTableColumn::*; use crate::table::table_column::LookupExtTableColumn; -use crate::table::table_column::LookupExtTableColumn::*; +use crate::table::table_column::MasterBaseTableColumn; use crate::vm::AlgebraicExecutionTrace; pub const BASE_WIDTH: usize = LookupBaseTableColumn::COUNT; pub const EXT_WIDTH: usize = LookupExtTableColumn::COUNT; pub const FULL_WIDTH: usize = BASE_WIDTH + EXT_WIDTH; -/// The lookup table for [Tip5](https://eprint.iacr.org/2023/107.pdf)'s split-and-lookup S-Box, -/// which uses the Fermat cube map over the finite field with 2^8 + 1 elements. -pub const LOOKUP_TABLE: [BFieldElement; 1 << 8] = lookup_table(); - -/// Compute the lookup table for the Fermat cube map. Helper method for the [`LOOKUP_TABLE`], -/// which should be used instead. -const fn lookup_table() -> [BFieldElement; 1 << 8] { - let mut lookup_table = [BFIELD_ZERO; 1 << 8]; - let mut i = 0; - while i < (1 << 8) { - lookup_table[i] = BFieldElement::new(Tip5::LOOKUP_TABLE[i] as u64); - i += 1; - } - lookup_table -} - pub struct LookupTable {} pub struct ExtLookupTable {} @@ -47,6 +31,26 @@ impl LookupTable { lookup_table: &mut ArrayViewMut2, aet: &AlgebraicExecutionTrace, ) { + assert!(lookup_table.nrows() >= 1 << 8); + + let lookup_input = Array1::from_iter((0_u64..1 << 8).map(BFieldElement::new)); + let lookup_output = Array1::from_iter( + (0..1 << 8).map(|i| BFieldElement::new(tip5::LOOKUP_TABLE[i] as u64)), + ); + let lookup_multiplicities = Array1::from_iter( + aet.lookup_table_lookup_multiplicities + .map(BFieldElement::new), + ); + + let lookup_input_column = + lookup_table.slice_mut(s![..1_usize << 8, LookIn.base_table_index()]); + lookup_input.move_into(lookup_input_column); + let lookup_output_column = + lookup_table.slice_mut(s![..1_usize << 8, LookOut.base_table_index()]); + lookup_output.move_into(lookup_output_column); + let lookup_multiplicities_column = + lookup_table.slice_mut(s![..1_usize << 8, LookupMultiplicity.base_table_index()]); + lookup_multiplicities.move_into(lookup_multiplicities_column); } pub fn pad_trace(lookup_table: &mut ArrayViewMut2) { From ed7ca4c0d9662390c6af22b5f2598759fae7061a Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 27 Feb 2023 15:44:09 +0100 Subject: [PATCH 06/49] start re-write of Hash Table specification --- specification/src/SUMMARY.md | 2 + specification/src/cascade-table.md | 1 + specification/src/hash-table.md | 260 +++++++++++++++------------- specification/src/lookup-table.md | 1 + triton-vm/src/table/table_column.rs | 114 ++++++------ 5 files changed, 207 insertions(+), 171 deletions(-) create mode 100644 specification/src/cascade-table.md create mode 100644 specification/src/lookup-table.md diff --git a/specification/src/SUMMARY.md b/specification/src/SUMMARY.md index 6b52e0d91..e853334c5 100644 --- a/specification/src/SUMMARY.md +++ b/specification/src/SUMMARY.md @@ -14,6 +14,8 @@ + [Random Access Memory Table](random-access-memory-table.md) + [Jump Stack Table](jump-stack-table.md) + [Hash Table](hash-table.md) + + [Cascade Table](cascade-table.md) + + [Lookup Table](lookup-table.md) + [U32 Table](u32-table.md) - [Table Linking](table-linking.md) + [Permutation Argument](permutation-argument.md) diff --git a/specification/src/cascade-table.md b/specification/src/cascade-table.md new file mode 100644 index 000000000..1cb43e1db --- /dev/null +++ b/specification/src/cascade-table.md @@ -0,0 +1 @@ +# Cascade Table diff --git a/specification/src/hash-table.md b/specification/src/hash-table.md index 0d707c835..1b5e01b60 100644 --- a/specification/src/hash-table.md +++ b/specification/src/hash-table.md @@ -3,29 +3,28 @@ The instruction `hash` hashes the OpStack's 10 top-most elements in one cycle. Similarly, the Sponge instructions `absorb_init`, `absorb`, and `squeeze` all complete in one cycle. The main processor achieves this by using a hash coprocessor. -The Hash Table is the arithmetization of that coprocessor. +The Hash Table is part of the arithmetization of that coprocessor, the other two parts being the [Cascade Table](cascade-table.md) and the [Lookup Table](lookup-table.md). Instruction `hash` and the Sponge instructions `absorb_init`, `absorb`, and `squeeze` are quite similar. -The main differences are in updates to the `state` registers between executions of Triton VM's pseudo-random permutation, [Rescue-XLIX](https://eprint.iacr.org/2020/1143.pdf). +The main differences are in updates to the `state` registers between executions of the pseudo-random permutation used in Triton VM, the permutation of [Tip5](https://eprint.iacr.org/2023/107.pdf). A summary of the four instructions' mechanics: - Instruction `hash` - 1. resets all state registers to 0, - 1. sets the eleventh state register, _i.e._, `st10`, to 1, - 1. adds the processor's stack registers `st0` through `st9` to the hash coprocessor's registers `state0` through `state9` - 1. executes the 8 rounds of Rescue-XLIX, + 1. sets all the hash coprocessor's rate registers (`state0` through `state9`) to equal the processor's stack registers `st0` through `st9`, + 1. sets all the hash coprocessor's capacity registers (`state10` through `state15`) to 1, + 1. executes the 5 rounds of the Tip5 permutation, 1. overwrites the processor's stack registers `st0` through `st4` with 0, and - 1. overwrites the processor's stack registers with the hash coprocessor's registers `st5` through `st9` with `state0` through `state4`. + 1. overwrites the processor's stack registers `st5` through `st9` with the hash coprocessor's registers `state0` through `state4`. - Instruction `absorb_init` - 1. resets all state registers to 0, - 1. adds the processor's stack registers `st0` through `st9` to the hash coprocessor's registers `state0` through `state9`, and - 1. executes the 8 rounds of Rescue-XLIX. + 1. sets all the hash coprocessor's rate registers (`state0` through `state9`) to equal the processor's stack registers `st0` through `st9`, + 1. sets all the hash coprocessor's capacity registers (`state10` through `state15`) to 0, and + 1. executes the 5 rounds of the Tip5 permutation. - Instruction `absorb` - 1. adds the processor's stack registers `st0` through `st9` to the hash coprocessor's registers `state0` through `state9`, and - 1. executes the 8 rounds of Rescue-XLIX. + 1. overwrites the hash coprocessor's rate registers (`state0` through `state9`) with the processor's stack registers `st0` through `st9`, and + 1. executes the 5 rounds of the Tip5 permutation. - Instruction `squeeze` - 1. overwrites the processor's stack registers `st0` through `st9` with the hash coprocessor's registers `state0` through `state9`, and - 1. executes the 8 rounds of Rescue-XLIX. + 1. overwrites the processor's stack registers `st0` through `st9` with the hash coprocessor's rate registers (`state0` through `state9`), and + 1. executes the 5 rounds of the Tip5 permutation. The Hash Table first records all Sponge instructions in the order the processor executed them. Then, the Hash Table records all `hash` instructions in the order the processor executed them. @@ -33,33 +32,33 @@ This allows the processor to execute `hash` instructions without affecting the S ## Base Columns -The Hash Table has 50 columns: -- one column `round_no` to indicate the round number, -- one column current instruction `CI`, holding the instruction the processor is currently executing, -- 16 state registers `state0` through `state15` to which the Rescue-XLIX rounds are applied, and -- 32 helper registers called `constant0A` through `constant15A` and `constant0B` through `constant15B` holding round constants. +The Hash Table has 66 base columns: -## Extension Columns - -The Hash Table has 5 extension columns: +- Round number indicator `round_no`, which can be one of $\{-1, 0, \dots, 5\}$. + The Tip5 permutation has 5 rounds, indexed $\{0, \dots, 4\}$. + The round number -1 indicates a padding row. + The round number 5 indicates that the Tip5 permutation has been applied in full. +- Current instruction `CI`, holding the instruction the processor is currently executing. +- 16 columns `state_i_highest_lkin`, `state_i_midhigh_lkin`, `state_i_midlow_lkin`, `state_i_lowest_lkin` for the to-be-looked-up value of `state0` through `state4`, each of which holds one 16-bit wide limb. +- 16 columns `state_i_highest_lkout`, `state_i_midhigh_lkout`, `state_i_midlow_lkout`, `state_i_lowest_lkout` for the looked-up value of `state0` through `state4`, each of which holds one 16-bit wide limb. +- 12 columns `state5` through `state15`. +- 4 columns `state_i_inv` establishing correct decomposition of `state_0_*_lkin` through `state_4_*_lkin` into 16-bit wide limbs. +- 16 columns `constant_i`, which hold the round constant for the round indicated by `RoundNumber`, or 0 if no round with this round number exists. -- `RunningEvaluationHashInput` -- `RunningEvaluationHashDigest` -- `RunningEvaluationSponge` +## Extension Columns -Each column corresponds to one Evaluation Argument. -The respective Evaluation Arguments establish: +The Hash Table has 19 extension columns: -1. whenever the [processor](processor-table.md) executes a `hash` instruction, the values of the stack's 10 top-most registers correspond to some row in the Hash Table with round index equal to 1. -1. after having executed a `hash` instruction, stack registers `st5` through `st9` in the [processor](processor-table.md) correspond to the digest computed in the Hash Coprocessor, i.e., the first 5 values of the Hash Table's row with round index equal to 9. -1. 1. whenever the processor executes an `absorb_init` instruction, the values of the stack's 10 top-most registers `st0` through `st9` in the processor correspond to some row in the Hash Table with round index equal to 1. - 1. whenever the processor executes an `absorb` instruction, the values of the stack's 10 top-most registers `st0` through `st9` in the processor correspond the difference of consecutive rows in the Hash Table where the lower row has round index equal to 1. - 1. whenever the processor executes a `squeeze` instruction, the values of the stack's 10 top-most registers correspond `st0` through `st9` in the processor correspond to the Sponge's current state, _i.e._, the first 10 values of the Hash Table's row with round index equal to 1. - 1. in all cases, the processor's register “current instruction” `ci` is correctly copied to the Hash Table's column `CI`. +- `RunningEvaluationHashInput` for the Evaluation Argument for copying the input to the hash function from the processor to the hash coprocessor, +- `RunningEvaluationHashDigest` for the Evaluation Argument for copying the hash digest from the hash coprocessor to the processor, +- `RunningEvaluationSponge` for the Evaluation Argument for copying the 10 next to-be-absorbed elements from the processor to the hash coprocessor or the 10 next squeezed elements from the hash coprocessor to the processor, depending on the instruction, +- 16 columns `state_i_*_LookupClientLogDerivative` (for `highest`, `midhigh`, `midlow`, and `lowest`) establishing correct lookup of the respective limbs in the [Cascade Table](cascade-table.md). ## Padding -Each padding row is the all-zero row with the exception of `CI`, which is the opcode of instruction `hash`. +Each padding row is the all-zero row with the exception of +- `round_no`, which is -1, and +- `CI`, which is the opcode of instruction `hash`. # Arithmetic Intermediate Representation @@ -100,139 +99,156 @@ Written as Disjunctive Normal Form, the same constraints can be expressed as: ## Consistency Constraints -1. If the round number is 0, then the current instruction is `hash`. -1. If the round number is 1 and the current instruction is `hash`, then register `state10` is 1. -1. If the round number is 1 and the current instruction is `absorb_init`, then register `state10` is 0. -1. If the round number is 1 and the current instruction is either `hash` or `absorb_init`, then register `state11` is 0. -1. If the round number is 1 and the current instruction is either `hash` or `absorb_init`, then register `state12` is 0. -1. If the round number is 1 and the current instruction is either `hash` or `absorb_init`, then register `state13` is 0. -1. If the round number is 1 and the current instruction is either `hash` or `absorb_init`, then register `state14` is 0. -1. If the round number is 1 and the current instruction is either `hash` or `absorb_init`, then register `state15` is 0. -1. The round constants adhere to the specification of Rescue Prime. +1. If the round number is -1, then the current instruction is `hash`. +1. If the round number is 0 and the current instruction is `hash`, then register `state10` is 1. +1. If the round number is 0 and the current instruction is `hash`, then register `state11` is 1. +1. If the round number is 0 and the current instruction is `hash`, then register `state12` is 1. +1. If the round number is 0 and the current instruction is `hash`, then register `state13` is 1. +1. If the round number is 0 and the current instruction is `hash`, then register `state14` is 1. +1. If the round number is 0 and the current instruction is `hash`, then register `state15` is 1. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state10` is 0. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state11` is 0. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state12` is 0. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state13` is 0. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state14` is 0. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state15` is 0. +1. If the round number is 0 and the current instruction is `absorb_init`, then register `state10` is 0. +1. The round constants adhere to the specification of Tip5. Written as Disjunctive Normal Form, the same constraints can be expressed as: -1. The round number is 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `hash`. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state10` is 1. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state10` is 0. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `absorb` or `squeeze` or `state11` is 0. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `absorb` or `squeeze` or `state12` is 0. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `absorb` or `squeeze` or `state13` is 0. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `absorb` or `squeeze` or `state14` is 0. -1. The round number is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI` is the opcode of `absorb` or `squeeze` or `state15` is 0. -1. The `constantiX` equals interpolant(`round_no`), where “interpolant” is the lowest-degree interpolant through (i, `constantiX`) for $1 \leqslant i \leqslant 9$, `X` $\in$ {A, B}. +1. The round number is 0 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash`. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state10` is 1. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state11` is 1. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state12` is 1. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state13` is 1. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state14` is 1. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `state15` is 1. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state10` is 0. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state11` is 0. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state12` is 0. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state13` is 0. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state14` is 0. +1. The round number is -1 or 1 or 2 or 3 or 4 or 5 or `CI` is the opcode of `hash` or `absorb` or `squeeze` or `state15` is 0. +1. The `constant_i` equals interpolant(`round_no`), where “interpolant” is the lowest-degree interpolant through (i, `constant_i`) for $-1 \leqslant i \leqslant 5$. ### Consistency Constraints as Polynomials -1. `(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
+1. `(round_no - 0)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
`·(CI - opcode(hash))` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
`·(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·(state10 - 1)` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+ `·(state11 - 1)` +1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+ `·(state12 - 1)` +1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+ `·(state13 - 1)` +1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+ `·(state14 - 1)` +1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+ `·(state15 - 1)` +1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
`·(CI - opcode(hash))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·state10` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
- `·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(hash))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·state11` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
- `·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(hash))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·state12` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
- `·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(hash))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·state13` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
- `·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(hash))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·state14` -1. `(round_no - 0)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no - 9)`
- `·(CI - opcode(absorb))·(CI - opcode(squeeze))`
+1. `(round_no + 1)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)`
+ `·(CI - opcode(hash))·(CI - opcode(absorb))·(CI - opcode(squeeze))`
`·state15` ## Transition Constraints -1. If the round number is 0, the round number in the next row is 0. -1. If the round number is 1, 2, 3, 4, 5, 6, 7, or 8, then the round number in the next row is incremented by 1. -1. If the round number is 9, the round number in the next row is either 0 or 1. +1. If the round number is -1, then the round number in the next row is -1. +1. If the round number is 1, 2, 3, or 4, then the round number in the next row is incremented by 1. +1. If the round number is 5, then the round number in the next row is either -1 or 0. 1. If the current instruction is `hash`, then the current instruction in the next row is `hash`. -1. If the round number is not 9, the current instruction in the next row is the current instruction in the current row. -1. If the round number in the next row is 1 and the current instruction in the next row is `absorb`, then the capacity's state registers don't change. -1. If the round number in the next row is 1 and the current instruction in the next row is `squeeze`, then none of the state registers change. -1. If the round number in the next row is 1 and the current instruction in the next row is `hash`, then `RunningEvaluationHashInput` accumulates the next row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪. Otherwise, it remains unchanged. -1. If the round number in the next row is 9 and the current instruction in the next row is `hash`, then `RunningEvaluationHashDigest` accumulates the next row with respect to challenges 🧄₀ through 🧄₄ and indeterminate 🪟. Otherwise, it remains unchanged. -1. 1. If the round number in the next row is 1 and the current instruction in the next row is `absorb_init` or `squeeze`, then `RunningEvaluationSponge` accumulates the next row with respect to challenges 🧅 and 🧄₀ through 🧄₉ and indeterminate 🧽. - 1. If the round number in the next row is 1 and the current instruction in the next row is `absorb`, then `RunningEvaluationSponge` accumulates `CI` in the next row and the difference of the next row and the current row with respect to challenges 🧅 and 🧄₀ through 🧄₉ and indeterminate 🧽. - 1. If the round number in the next row is not 1, then `RunningEvaluationSponge` remains unchanged. +1. If the round number is not 5, the current instruction in the next row is the current instruction in the current row. +1. If the round number in the next row is 0 and the current instruction in the next row is `absorb`, then the capacity's state registers don't change. +1. If the round number in the next row is 0 and the current instruction in the next row is `squeeze`, then none of the state registers change. +1. If the round number in the next row is 0 and the current instruction in the next row is `hash`, then `RunningEvaluationHashInput` accumulates the next row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪. Otherwise, it remains unchanged. +1. If the round number in the next row is 5 and the current instruction in the next row is `hash`, then `RunningEvaluationHashDigest` accumulates the next row with respect to challenges 🧄₀ through 🧄₄ and indeterminate 🪟. Otherwise, it remains unchanged. +1. 1. If the round number in the next row is 0 and the current instruction in the next row is `absorb_init`, `absorb`, or `squeeze`, then `RunningEvaluationSponge` accumulates the next row with respect to challenges 🧅 and 🧄₀ through 🧄₉ and indeterminate 🧽. + 1. If the round number in the next row is not 0, then `RunningEvaluationSponge` remains unchanged. 1. If the current instruction in the next row is `hash`, then `RunningEvaluationSponge` remains unchanged. -1. If the round number is 1, the `state` registers adhere to the rules of applying Rescue-XLIX round 1. -1. If the round number is 2, the `state` registers adhere to the rules of applying Rescue-XLIX round 2. -1. If the round number is 3, the `state` registers adhere to the rules of applying Rescue-XLIX round 3. -1. If the round number is 4, the `state` registers adhere to the rules of applying Rescue-XLIX round 4. -1. If the round number is 5, the `state` registers adhere to the rules of applying Rescue-XLIX round 5. -1. If the round number is 6, the `state` registers adhere to the rules of applying Rescue-XLIX round 6. -1. If the round number is 7, the `state` registers adhere to the rules of applying Rescue-XLIX round 7. -1. If the round number is 8, the `state` registers adhere to the rules of applying Rescue-XLIX round 8. +1. If the round number is 0, the `state` registers adhere to the rules of applying round 0 of the Tip5 permutation. +1. If the round number is 1, the `state` registers adhere to the rules of applying round 1 of the Tip5 permutation. +1. If the round number is 2, the `state` registers adhere to the rules of applying round 2 of the Tip5 permutation. +1. If the round number is 3, the `state` registers adhere to the rules of applying round 3 of the Tip5 permutation. +1. If the round number is 4, the `state` registers adhere to the rules of applying round 4 of the Tip5 permutation. Written as Disjunctive Normal Form, the same constraints can be expressed as: -1. `round_no` is 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `round_no'` is 0. -1. `round_no` is 0 or 9 or `round_no'` is `round_no` + 1. -1. `round_no` is 0 or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or `round_no'` is 0 or 1. +1. `round_no` is 0 or 1 or 2 or 3 or 4 or 5 or `round_no'` is -1. +1. `round_no` is -1 or 5 or `round_no'` is `round_no` + 1. +1. `round_no` is -1 or 0 or 1 or 2 or 3 or 4 or `round_no'` is -1 or 0. 1. `CI` is the opcode of `absorb_init` or `absorb` or `squeeze` or `CI'` is the opcode of `hash`. -1. `round_no` is 9 or `CI'` is `CI`. -1. `round_no'` is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI'` is the opcode of `hash` or `absorb_init` or `squeeze` or the $🧄_i$-randomized sum of differences of the state registers `state10` through `state15` in the next row and the current row is 0. -1. `round_no'` is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI'` is the opcode of `hash` or `absorb_init` or `absorb` or the $🧄_i$-randomized sum of differences of all state registers in the next row and the current row is 0. -1. (`round_no'` is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI'` is the opcode of `absorb_init` or `absorb` or `squeeze` or `RunningEvaluationHashInput` accumulates the next row)
- and (`round_no'` is 1 or `RunningEvaluationHashInput` remains unchanged)
+1. `round_no` is 5 or `CI'` is `CI`. +1. `round_no'` is -1 or 1 or 2 or 3 or 4 or 5 or `CI'` is the opcode of `hash` or `absorb_init` or `squeeze` or the $🧄_i$-randomized sum of differences of the state registers `state10` through `state15` in the next row and the current row is 0. +1. `round_no'` is -1 or 1 or 2 or 3 or 4 or 5 or `CI'` is the opcode of `hash` or `absorb_init` or `absorb` or the $🧄_i$-randomized sum of differences of all state registers in the next row and the current row is 0. +1. (`round_no'` is -1 or 1 or 2 or 3 or 4 or 5 or `CI'` is the opcode of `absorb_init` or `absorb` or `squeeze` or `RunningEvaluationHashInput` accumulates the next row)
+ and (`round_no'` is 0 or `RunningEvaluationHashInput` remains unchanged)
and (`CI'` is the opcode of `hash` or `RunningEvaluationHashInput` remains unchanged). -1. (`round_no'` is 0 or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or `CI'` is the opcode of `absorb_init` or `absorb` or `squeeze` or `RunningEvaluationHashDigest` accumulates the next row)
- and (`round_no'` is 9 or `RunningEvaluationHashDigest` remains unchanged)
+1. (`round_no'` is -1 or 0 or 1 or 2 or 3 or 4 or `CI'` is the opcode of `absorb_init` or `absorb` or `squeeze` or `RunningEvaluationHashDigest` accumulates the next row)
+ and (`round_no'` is 5 or `RunningEvaluationHashDigest` remains unchanged)
and (`CI'` is the opcode of `hash` or `RunningEvaluationHashDigest` remains unchanged). -1. 1. (`round_no'` is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI'` is the opcode of `hash` or `absorb` or `RunningEvaluationSponge` accumulates the next row) - 1. and (`round_no'` is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or `CI'` is the opcode of `hash` or `absorb_init` or `squeeze` or `RunningEvaluationSponge` accumulates the difference of the next row and the current row) - 1. and (`round_no'` is 1 or `RunningEvaluationSponge` remains unchanged) + +1. 1. (`round_no'` is -1 or 1 or 2 or 3 or 4 or 5 or `CI'` is the opcode of `hash` or `RunningEvaluationSponge` accumulates the next row) + 1. and (`round_no'` is 0 or `RunningEvaluationSponge` remains unchanged) 1. and (`CI'` is the opcode of `absorb_init` or `absorb` or `squeeze` or `RunningEvaluationSponge` remains unchanged). -1. `round_no` is 0 or 2 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 1. -1. `round_no` is 0 or 1 or 3 or 4 or 5 or 6 or 7 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 2. -1. `round_no` is 0 or 1 or 2 or 4 or 5 or 6 or 7 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 3. -1. `round_no` is 0 or 1 or 2 or 3 or 5 or 6 or 7 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 4. -1. `round_no` is 0 or 1 or 2 or 3 or 4 or 6 or 7 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 5. -1. `round_no` is 0 or 1 or 2 or 3 or 4 or 5 or 7 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 6. -1. `round_no` is 0 or 1 or 2 or 3 or 4 or 5 or 6 or 8 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 7. -1. `round_no` is 0 or 1 or 2 or 3 or 4 or 5 or 6 or 7 or 9 or the `state` registers adhere to the rules of applying Rescue-XLIX round 8. +1. `round_no` is -1 or 1 or 2 or 3 or 4 or 5 or the `state` registers adhere to the rules of applying round 0 of the Tip5 permutation. +1. `round_no` is -1 or 0 or 2 or 3 or 4 or 5 or the `state` registers adhere to the rules of applying round 1 of the Tip5 permutation. +1. `round_no` is -1 or 0 or 1 or 3 or 4 or 5 or the `state` registers adhere to the rules of applying round 2 of the Tip5 permutation. +1. `round_no` is -1 or 0 or 1 or 2 or 4 or 5 or the `state` registers adhere to the rules of applying round 3 of the Tip5 permutation. +1. `round_no` is -1 or 0 or 1 or 2 or 3 or 5 or the `state` registers adhere to the rules of applying round 4 of the Tip5 permutation. ### Transition Constraints as Polynomials -1. `(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no - 8)·(round_no-9)·(round_no' - 0)` -1. `(round_no - 0)·(round_no - 9)·(round_no' - round_no - 1)` -1. `(round_no - 0)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no - 5)·(round_no - 6)·(round_no - 7)·(round_no-8)·(round_no' - 0)·(round_no' - 1)` +1. `(round_no - 0)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no' + 1)` +1. `(round_no + 1)·(round_no - 5)·(round_no' - round_no - 1)` +1. `(round_no + 1)·(round_no - 0)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(round_no' + 1)·(round_no' - 0)` 1. `(CI - opcode(absorb_init))·(CI - opcode(absorb))·(CI - opcode(squeeze))·(CI' - opcode(hash))` -1. `(round_no - 9)·(CI' - CI)` -1. `(round_no' - 0)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)·(round_no' - 6)·(round_no' - 7)·(round_no' - 8)·(round_no' - 9)`
+1. `(round_no - 5)·(CI' - CI)` +1. `(round_no' + 1)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)`
`·(CI' - opcode(hash))·(CI' - opcode(absorb_init))·(CI' - opcode(squeeze))`
`·(🧄₁₀·(st10' - st10) + 🧄₁₁·(st11' - st11) + 🧄₁₂·(st12' - st12) + 🧄₁₃·(st13' - st13) + 🧄₁₄·(st14' - st14) + 🧄₁₅·(st15' - st15))` -1. `(round_no' - 0)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)·(round_no' - 6)·(round_no' - 7)·(round_no' - 8)·(round_no' - 9)`
+1. `(round_no' + 1)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)`
`·(CI' - opcode(hash))·(CI' - opcode(absorb_init))·(CI' - opcode(absorb))`
`·(🧄₀·(st0' - st0) + 🧄₁·(st1' - st1) + 🧄₂·(st2' - st2) + 🧄₃·(st3' - st3) + 🧄₄·(st4' - st4) + 🧄₅·(st5' - st5) + 🧄₆·(st6' - st6) + 🧄₇·(st7' - st7) + 🧄₈·(st8' - st8) + 🧄₉·(st9' - st9) + 🧄₁₀·(st10' - st10) + 🧄₁₁·(st11' - st11) + 🧄₁₂·(st12' - st12) + 🧄₁₃·(st13' - st13) + 🧄₁₄·(st14' - st14) + 🧄₁₅·(st15' - st15))` -1. `(round_no' - 0)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)·(round_no' - 6)·(round_no' - 7)·(round_no' - 8)·(round_no' - 9)`
+1. `(round_no' + 1)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)`
`·(CI' - opcode(absorb_init))·(CI' - opcode(absorb))·(CI' - opcode(squeeze))`
`·(RunningEvaluationHashInput' - 🚪·RunningEvaluationHashInput - 🧄₀·st0' - 🧄₁·st1' - 🧄₂·st2' - 🧄₃·st3' - 🧄₄·st4' - 🧄₅·st5' - 🧄₆·st6' - 🧄₇·st7' - 🧄₈·st8' - 🧄₉·st9')`
- `+ (round_no' - 1)·(RunningEvaluationHashInput' - RunningEvaluationHashInput)`
+ `+ round_no'·(RunningEvaluationHashInput' - RunningEvaluationHashInput)`
`+ (CI' - opcode(hash))·(RunningEvaluationHashInput' - RunningEvaluationHashInput)` -1. `(round_no' - 0)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)·(round_no' - 6)·(round_no' - 7)·(round_no' - 8)`
+1. `(round_no' + 1)·(round_no' - 0)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)`
`·(CI' - opcode(absorb_init))·(CI' - opcode(absorb))·(CI' - opcode(squeeze))`
`·(RunningEvaluationHashDigest' - 🪟·RunningEvaluationHashDigest - 🧄₀·st0' - 🧄₁·st1' - 🧄₂·st2' - 🧄₃·st3' - 🧄₄·st4')`
- `+ (round_no' - 9)·(RunningEvaluationHashDigest' - RunningEvaluationHashDigest)`
+ `+ (round_no' - 5)·(RunningEvaluationHashDigest' - RunningEvaluationHashDigest)`
`+ (CI' - opcode(hash))·(RunningEvaluationHashDigest' - RunningEvaluationHashDigest)` -1. 1. `(round_no' - 0)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)·(round_no' - 6)·(round_no' - 7)·(round_no' - 8)·(round_no' - 9)`
- `·(CI' - opcode(hash))·(CI' - opcode(absorb))`
+ +1. 1. `(round_no' + 1)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)`
+ `·(CI' - opcode(hash))`
`·(RunningEvaluationSponge' - 🧽·RunningEvaluationSponge - 🧅·CI' - 🧄₀·st0' - 🧄₁·st1' - 🧄₂·st2' - 🧄₃·st3' - 🧄₄·st4' - 🧄₅·st5' - 🧄₆·st6' - 🧄₇·st7' - 🧄₈·st8' - 🧄₉·st9')`
- 1. `+ (round_no' - 0)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)·(round_no' - 6)·(round_no' - 7)·(round_no' - 8)·(round_no' - 9)`
- `·(CI' - opcode(hash))·(CI' - opcode(absorb_init))·(CI' - opcode(squeeze))`
- `·(RunningEvaluationSponge' - 🧽·RunningEvaluationSponge - 🧅·CI' - 🧄₀·(st0' - st0) - 🧄₁·(st1' - st1) - 🧄₂·(st2' - st2) - 🧄₃·(st3' - st3) - 🧄₄·(st4' - st4) - 🧄₅·(st5' - st5) - 🧄₆·(st6' - st6) - 🧄₇·(st7' - st7) - 🧄₈·(st8' - st8) - 🧄₉·(st9' - st9))`
- 1. `+ (round_no' - 1)·(RunningEvaluationSponge' - RunningEvaluationSponge)`
+ 1. `+ (round_no' - 0)·(RunningEvaluationSponge' - RunningEvaluationSponge)`
1. `+ (CI' - opcode(absorb_init))·(CI' - opcode(absorb))·(CI' - opcode(squeeze))·(RunningEvaluationSponge' - RunningEvaluationSponge)` 1. The remaining constraints are left as an exercise to the reader. - For hints, see the [Rescue-Prime Systematization of Knowledge, Sections 2.4 & 2.5](https://eprint.iacr.org/2020/1143.pdf#page=5). + For hints, see the [Tip5 paper](https://eprint.iacr.org/2023/107.pdf). ## Terminal Constraints diff --git a/specification/src/lookup-table.md b/specification/src/lookup-table.md new file mode 100644 index 000000000..00b3b2a4a --- /dev/null +++ b/specification/src/lookup-table.md @@ -0,0 +1 @@ +# Lookup Table diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index 3c18a6125..d9f0af152 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -192,56 +192,72 @@ pub enum JumpStackExtTableColumn { #[repr(usize)] #[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] pub enum HashBaseTableColumn { - ROUNDNUMBER, + RoundNumber, CI, - STATE0, - STATE1, - STATE2, - STATE3, - STATE4, - STATE5, - STATE6, - STATE7, - STATE8, - STATE9, - STATE10, - STATE11, - STATE12, - STATE13, - STATE14, - STATE15, - CONSTANT0A, - CONSTANT1A, - CONSTANT2A, - CONSTANT3A, - CONSTANT4A, - CONSTANT5A, - CONSTANT6A, - CONSTANT7A, - CONSTANT8A, - CONSTANT9A, - CONSTANT10A, - CONSTANT11A, - CONSTANT12A, - CONSTANT13A, - CONSTANT14A, - CONSTANT15A, - CONSTANT0B, - CONSTANT1B, - CONSTANT2B, - CONSTANT3B, - CONSTANT4B, - CONSTANT5B, - CONSTANT6B, - CONSTANT7B, - CONSTANT8B, - CONSTANT9B, - CONSTANT10B, - CONSTANT11B, - CONSTANT12B, - CONSTANT13B, - CONSTANT14B, - CONSTANT15B, + State0HighestLkIn, + State0MidHighLkIn, + State0MidLowLkIn, + State0LowestLkIn, + State1HighestLkIn, + State1MidHighLkIn, + State1MidLowLkIn, + State1LowestLkIn, + State2HighestLkIn, + State2MidHighLkIn, + State2MidLowLkIn, + State2LowestLkIn, + State3HighestLkIn, + State3MidHighLkIn, + State3MidLowLkIn, + State3LowestLkIn, + State0HighestLkOut, + State0MidHighLkOut, + State0MidLowLkOut, + State0LowestLkOut, + State1HighestLkOut, + State1MidHighLkOut, + State1MidLowLkOut, + State1LowestLkOut, + State2HighestLkOut, + State2MidHighLkOut, + State2MidLowLkOut, + State2LowestLkOut, + State3HighestLkOut, + State3MidHighLkOut, + State3MidLowLkOut, + State3LowestLkOut, + State4, + State5, + State6, + State7, + State8, + State9, + State10, + State11, + State12, + State13, + State14, + State15, + State0Inv, + State1Inv, + State2Inv, + State3Inv, + Constant0, + Constant1, + Constant2, + Constant3, + Constant4, + Constant5, + Constant6, + Constant7, + Constant8, + Constant9, + Constant10, + Constant11, + Constant12, + Constant13, + Constant14, + Constant15, } #[repr(usize)] From 021b52543af6eca6e444580c5b75c471b055ffa8 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 28 Feb 2023 14:41:39 +0100 Subject: [PATCH 07/49] use Tip5 when executing the VM, fully populate rows for Hash Table --- triton-vm/src/table/cascade_table.rs | 10 ++ triton-vm/src/table/hash_table.rs | 8 +- triton-vm/src/vm.rs | 247 ++++++++++++++++++--------- 3 files changed, 184 insertions(+), 81 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index f3baf1369..c21cbd19b 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -4,6 +4,7 @@ use ndarray::ArrayViewMut2; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; +use twenty_first::shared_math::tip5; use twenty_first::shared_math::x_field_element::XFieldElement; use crate::table::challenges::Challenges; @@ -44,6 +45,15 @@ impl CascadeTable { assert_eq!(EXT_WIDTH, ext_table.ncols()); assert_eq!(base_table.nrows(), ext_table.nrows()); } + + pub fn lookup_16_bit_limb(to_look_up: u16) -> BFieldElement { + let to_look_up_lo = (to_look_up & 0xff) as usize; + let to_look_up_hi = ((to_look_up >> 8) & 0xff) as usize; + let looked_up_lo = tip5::LOOKUP_TABLE[to_look_up_lo] as u64; + let looked_up_hi = tip5::LOOKUP_TABLE[to_look_up_hi] as u64; + let looked_up = (looked_up_hi << 8) | looked_up_lo; + BFieldElement::from_raw_u64(looked_up) + } } impl ExtCascadeTable { diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index a234c395d..7a5acc006 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -43,7 +43,7 @@ pub const BASE_WIDTH: usize = HashBaseTableColumn::COUNT; pub const EXT_WIDTH: usize = HashExtTableColumn::COUNT; pub const FULL_WIDTH: usize = BASE_WIDTH + EXT_WIDTH; -pub const NUM_ROUND_CONSTANTS: usize = STATE_SIZE * 2; +pub const NUM_ROUND_CONSTANTS: usize = STATE_SIZE; pub const TOTAL_NUM_CONSTANTS: usize = NUM_ROUND_CONSTANTS * NUM_ROUNDS; #[derive(Debug, Clone)] @@ -68,7 +68,7 @@ impl ExtHashTable { let running_evaluation_initial = circuit_builder.x_constant(EvalArg::default_initial()); - let round_number = base_row(ROUNDNUMBER); + let round_number = base_row(RoundNumber); let ci = base_row(CI); let running_evaluation_hash_input = ext_row(HashInputRunningEvaluation); let running_evaluation_hash_digest = ext_row(HashDigestRunningEvaluation); @@ -478,8 +478,8 @@ impl ExtHashTable { * (ci_next.clone() - opcode_squeeze.clone()); let running_evaluation_hash_input_remains = running_evaluation_hash_input_next.clone() - running_evaluation_hash_input.clone(); - let xlix_input = state_next[0..2 * DIGEST_LENGTH].to_owned(); - let compressed_row_from_processor = xlix_input + let tip5_input = state_next[0..2 * DIGEST_LENGTH].to_owned(); + let compressed_row_from_processor = tip5_input .into_iter() .zip_eq(state_weights[0..2 * DIGEST_LENGTH].iter()) .map(|(state, weight)| weight.clone() * state) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 053fb2434..476fb79cc 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -7,24 +7,16 @@ use itertools::Itertools; use ndarray::s; use ndarray::Array1; use ndarray::Array2; -use ndarray::ArrayBase; use ndarray::Axis; -use ndarray::Ix2; -use ndarray::OwnedRepr; use num_traits::One; use num_traits::Zero; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ZERO; use twenty_first::shared_math::other::log_2_floor; use twenty_first::shared_math::rescue_prime_digest::DIGEST_LENGTH; -use twenty_first::shared_math::rescue_prime_regular::RescuePrimeRegular; -use twenty_first::shared_math::rescue_prime_regular::RescuePrimeRegularState; -use twenty_first::shared_math::rescue_prime_regular::NUM_ROUNDS; -use twenty_first::shared_math::rescue_prime_regular::RATE; -use twenty_first::shared_math::rescue_prime_regular::ROUND_CONSTANTS; -use twenty_first::shared_math::rescue_prime_regular::STATE_SIZE; use twenty_first::shared_math::tip5; use twenty_first::shared_math::tip5::Tip5; +use twenty_first::shared_math::tip5::Tip5State; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; use twenty_first::util_types::algebraic_hasher::Domain; @@ -41,14 +33,12 @@ use crate::error::vm_fail; use crate::error::InstructionError::InstructionPointerOverflow; use crate::error::InstructionError::*; use crate::op_stack::OpStack; +use crate::table::cascade_table::CascadeTable; use crate::table::hash_table; use crate::table::hash_table::NUM_ROUND_CONSTANTS; use crate::table::processor_table; use crate::table::processor_table::ProcessorTraceRow; -use crate::table::table_column::HashBaseTableColumn::CI; -use crate::table::table_column::HashBaseTableColumn::CONSTANT0A; -use crate::table::table_column::HashBaseTableColumn::ROUNDNUMBER; -use crate::table::table_column::HashBaseTableColumn::STATE0; +use crate::table::table_column::HashBaseTableColumn::*; use crate::table::table_column::MasterBaseTableColumn; use crate::table::table_column::ProcessorBaseTableColumn; @@ -89,7 +79,7 @@ pub struct VMState<'pgm> { /// absorbing. /// Note that this is the _full_ state, including capacity. The capacity should never be /// exposed outside of the VM. - pub sponge_state: [BFieldElement; STATE_SIZE], + pub sponge_state: [BFieldElement; tip5::STATE_SIZE], // Bookkeeping /// Indicates whether the terminating instruction `halt` has been executed. @@ -103,10 +93,10 @@ pub enum VMOutput { /// Trace of the state registers for hash coprocessor table when executing instruction `hash` /// or any of the Sponge instructions `absorb_init`, `absorb`, `squeeze`. - /// One row per round in the XLIX permutation. - XlixTrace( + /// One row per round in the Tip5 permutation. + Tip5Trace( Instruction, - Box<[[BFieldElement; STATE_SIZE]; 1 + NUM_ROUNDS]>, + Box<[[BFieldElement; tip5::STATE_SIZE]; 1 + tip5::NUM_ROUNDS]>, ), /// Executed u32 instruction as well as its left-hand side and right-hand side @@ -320,11 +310,11 @@ impl<'pgm> VMState<'pgm> { } Hash => { - let to_hash = self.op_stack.pop_n::<{ 2 * DIGEST_LENGTH }>()?; - let mut hash_input = RescuePrimeRegularState::new(Domain::FixedLength).state; - hash_input[..2 * DIGEST_LENGTH].copy_from_slice(&to_hash); - let xlix_trace = RescuePrimeRegular::trace(hash_input); - let hash_output = &xlix_trace[xlix_trace.len() - 1][0..DIGEST_LENGTH]; + let to_hash = self.op_stack.pop_n::<{ tip5::RATE }>()?; + let mut hash_input = Tip5State::new(Domain::FixedLength); + hash_input.state[..tip5::RATE].copy_from_slice(&to_hash); + let tip5_trace = Tip5::trace(&mut hash_input); + let hash_output = &tip5_trace[tip5_trace.len() - 1][0..DIGEST_LENGTH]; for i in (0..DIGEST_LENGTH).rev() { self.op_stack.push(hash_output[i]); @@ -333,45 +323,49 @@ impl<'pgm> VMState<'pgm> { self.op_stack.push(BFieldElement::zero()); } - vm_output = Some(VMOutput::XlixTrace(Hash, Box::new(xlix_trace))); + vm_output = Some(VMOutput::Tip5Trace(Hash, Box::new(tip5_trace))); self.instruction_pointer += 1; } AbsorbInit | Absorb => { // fetch top elements but don't alter the stack - let to_absorb = self.op_stack.pop_n::<{ RATE }>()?; - for i in (0..RATE).rev() { + let to_absorb = self.op_stack.pop_n::<{ tip5::RATE }>()?; + for i in (0..tip5::RATE).rev() { self.op_stack.push(to_absorb[i]); } if self.current_instruction()? == AbsorbInit { - self.sponge_state = RescuePrimeRegularState::new(Domain::VariableLength).state; + self.sponge_state = Tip5State::new(Domain::VariableLength).state; } - self.sponge_state[..RATE] + self.sponge_state[..tip5::RATE] .iter_mut() .zip_eq(to_absorb.iter()) .for_each(|(sponge_state_element, &to_absorb_element)| { *sponge_state_element += to_absorb_element; }); - let xlix_trace = RescuePrimeRegular::trace(self.sponge_state); - self.sponge_state = xlix_trace.last().unwrap().to_owned(); + let tip5_trace = Tip5::trace(&mut Tip5State { + state: self.sponge_state, + }); + self.sponge_state = tip5_trace.last().unwrap().to_owned(); - vm_output = Some(VMOutput::XlixTrace( + vm_output = Some(VMOutput::Tip5Trace( self.current_instruction()?, - Box::new(xlix_trace), + Box::new(tip5_trace), )); self.instruction_pointer += 1; } Squeeze => { - let _ = self.op_stack.pop_n::<{ RATE }>()?; - for i in (0..RATE).rev() { + let _ = self.op_stack.pop_n::<{ tip5::RATE }>()?; + for i in (0..tip5::RATE).rev() { self.op_stack.push(self.sponge_state[i]); } - let xlix_trace = RescuePrimeRegular::trace(self.sponge_state); - self.sponge_state = xlix_trace.last().unwrap().to_owned(); + let tip5_trace = Tip5::trace(&mut Tip5State { + state: self.sponge_state, + }); + self.sponge_state = tip5_trace.last().unwrap().to_owned(); - vm_output = Some(VMOutput::XlixTrace(Squeeze, Box::new(xlix_trace))); + vm_output = Some(VMOutput::Tip5Trace(Squeeze, Box::new(tip5_trace))); self.instruction_pointer += 1; } @@ -861,11 +855,11 @@ pub fn simulate( Ok(vm_output) => vm_output, }; match vm_output { - Some(VMOutput::XlixTrace(Instruction::Hash, xlix_trace)) => { - aet.append_hash_trace(*xlix_trace) + Some(VMOutput::Tip5Trace(Instruction::Hash, tip5_trace)) => { + aet.append_hash_trace(*tip5_trace) } - Some(VMOutput::XlixTrace(instruction, xlix_trace)) => { - aet.append_sponge_trace(instruction, *xlix_trace) + Some(VMOutput::Tip5Trace(instruction, tip5_trace)) => { + aet.append_sponge_trace(instruction, *tip5_trace) } Some(VMOutput::U32TableEntries(u32_entries)) => { for u32_entry in u32_entries { @@ -938,12 +932,12 @@ pub struct AlgebraicExecutionTrace { /// Records the state of the processor after each instruction. pub processor_trace: Array2, - /// For the `hash` instruction, the hash trace records the internal state of the XLIX + /// For the `hash` instruction, the hash trace records the internal state of the Tip5 /// permutation for each round. pub hash_trace: Array2, /// For the Sponge instructions, i.e., `absorb_init`, `absorb`, and `squeeze`, the Sponge - /// trace records the internal state of the XLIX permutation for each round. + /// trace records the internal state of the Tip5 permutation for each round. pub sponge_trace: Array2, /// The u32 entries hold all pairs of BFieldElements that were written to the U32 Table, @@ -975,10 +969,10 @@ impl AlgebraicExecutionTrace { pub fn append_hash_trace( &mut self, - hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], ) { self.increase_lookup_multiplicities(hash_permutation_trace); - let mut hash_trace_addendum = Self::add_round_number_and_constants(hash_permutation_trace); + let mut hash_trace_addendum = Self::convert_to_hash_table_rows(hash_permutation_trace); hash_trace_addendum .slice_mut(s![.., CI.base_table_index()]) .fill(Instruction::Hash.opcode_b()); @@ -990,15 +984,14 @@ impl AlgebraicExecutionTrace { pub fn append_sponge_trace( &mut self, instruction: Instruction, - hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], ) { assert!(matches!( instruction, Instruction::AbsorbInit | Instruction::Absorb | Instruction::Squeeze )); self.increase_lookup_multiplicities(hash_permutation_trace); - let mut sponge_trace_addendum = - Self::add_round_number_and_constants(hash_permutation_trace); + let mut sponge_trace_addendum = Self::convert_to_hash_table_rows(hash_permutation_trace); sponge_trace_addendum .slice_mut(s![.., CI.base_table_index()]) .fill(instruction.opcode_b()); @@ -1013,7 +1006,7 @@ impl AlgebraicExecutionTrace { /// and increases the multiplicities accordingly fn increase_lookup_multiplicities( &mut self, - hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], ) { for row in hash_permutation_trace.iter().rev().skip(1) { for state_element in row[0..tip5::NUM_SPLIT_AND_LOOKUP].iter() { @@ -1031,48 +1024,148 @@ impl AlgebraicExecutionTrace { } } - /// Given an XLIX trace, this function adds + /// Given a trace of the Tip5 permutation, construct a trace corresponding to the columns of + /// the Hash Table. This includes /// - /// 1. the round number, and - /// 2. the relevant round constants + /// - adding the round number + /// - adding the round constants, + /// - decomposing the first [`tip5::NUM_SPLIT_AND_LOOKUP`] (== 4) state elements into their + /// constituent limbs, + /// - setting the inverse-or-zero for proving correct limb decomposition, and + /// - adding the looked-up value for each limb. /// - /// to each row. The result has the same width as the Hash Table. The current instruction is not - /// set. - fn add_round_number_and_constants( - xlix_trace: [[BFieldElement; 16]; 9], - ) -> ArrayBase, Ix2> { - let mut hash_trace_addendum = Array2::default([NUM_ROUNDS + 1, hash_table::BASE_WIDTH]); - for (row_idx, mut row) in hash_trace_addendum.rows_mut().into_iter().enumerate() { - let round_number = row_idx + 1; - let trace_row = xlix_trace[row_idx]; - let round_constants = Self::rescue_xlix_round_constants_by_round_number(round_number); - row[ROUNDNUMBER.base_table_index()] = BFieldElement::from(row_idx as u64 + 1); - for st_idx in 0..STATE_SIZE { - row[STATE0.base_table_index() + st_idx] = trace_row[st_idx]; - } - for rc_idx in 0..NUM_ROUND_CONSTANTS { - row[CONSTANT0A.base_table_index() + rc_idx] = round_constants[rc_idx]; - } + /// The current instruction is not set. + fn convert_to_hash_table_rows( + hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], + ) -> Array2 { + let mut hash_trace_addendum = Array2::zeros([tip5::NUM_ROUNDS + 1, hash_table::BASE_WIDTH]); + for (round_number, mut row) in hash_trace_addendum.rows_mut().into_iter().enumerate() { + let trace_row = hash_permutation_trace[round_number]; + row[RoundNumber.base_table_index()] = BFieldElement::from(round_number as u64); + + let st_0_raw_limbs = trace_row[0].raw_u16s(); + let st_0_look_in_split = + st_0_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State0HighestLkIn.base_table_index()] = st_0_look_in_split[0]; + row[State0MidHighLkIn.base_table_index()] = st_0_look_in_split[1]; + row[State0MidLowLkIn.base_table_index()] = st_0_look_in_split[2]; + row[State0LowestLkIn.base_table_index()] = st_0_look_in_split[3]; + + let st_0_look_out_split = st_0_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State0HighestLkOut.base_table_index()] = st_0_look_out_split[0]; + row[State0MidHighLkOut.base_table_index()] = st_0_look_out_split[1]; + row[State0MidLowLkOut.base_table_index()] = st_0_look_out_split[2]; + row[State0LowestLkOut.base_table_index()] = st_0_look_out_split[3]; + + let st_1_raw_limbs = trace_row[1].raw_u16s(); + let st_1_look_in_split = + st_1_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State1HighestLkIn.base_table_index()] = st_1_look_in_split[0]; + row[State1MidHighLkIn.base_table_index()] = st_1_look_in_split[1]; + row[State1MidLowLkIn.base_table_index()] = st_1_look_in_split[2]; + row[State1LowestLkIn.base_table_index()] = st_1_look_in_split[3]; + + let st_1_look_out_split = st_1_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State1HighestLkOut.base_table_index()] = st_1_look_out_split[0]; + row[State1MidHighLkOut.base_table_index()] = st_1_look_out_split[1]; + row[State1MidLowLkOut.base_table_index()] = st_1_look_out_split[2]; + row[State1LowestLkOut.base_table_index()] = st_1_look_out_split[3]; + + let st_2_raw_limbs = trace_row[2].raw_u16s(); + let st_2_look_in_split = + st_2_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State2HighestLkIn.base_table_index()] = st_2_look_in_split[0]; + row[State2MidHighLkIn.base_table_index()] = st_2_look_in_split[1]; + row[State2MidLowLkIn.base_table_index()] = st_2_look_in_split[2]; + row[State2LowestLkIn.base_table_index()] = st_2_look_in_split[3]; + + let st_2_look_out_split = st_2_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State2HighestLkOut.base_table_index()] = st_2_look_out_split[0]; + row[State2MidHighLkOut.base_table_index()] = st_2_look_out_split[1]; + row[State2MidLowLkOut.base_table_index()] = st_2_look_out_split[2]; + row[State2LowestLkOut.base_table_index()] = st_2_look_out_split[3]; + + let st_3_raw_limbs = trace_row[3].raw_u16s(); + let st_3_look_in_split = + st_3_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State3HighestLkIn.base_table_index()] = st_3_look_in_split[0]; + row[State3MidHighLkIn.base_table_index()] = st_3_look_in_split[1]; + row[State3MidLowLkIn.base_table_index()] = st_3_look_in_split[2]; + row[State3LowestLkIn.base_table_index()] = st_3_look_in_split[3]; + + let st_3_look_out_split = st_3_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State3HighestLkOut.base_table_index()] = st_3_look_out_split[0]; + row[State3MidHighLkOut.base_table_index()] = st_3_look_out_split[1]; + row[State3MidLowLkOut.base_table_index()] = st_3_look_out_split[2]; + row[State3LowestLkOut.base_table_index()] = st_3_look_out_split[3]; + + row[State4.base_table_index()] = trace_row[4]; + row[State5.base_table_index()] = trace_row[5]; + row[State6.base_table_index()] = trace_row[6]; + row[State7.base_table_index()] = trace_row[7]; + row[State8.base_table_index()] = trace_row[8]; + row[State9.base_table_index()] = trace_row[9]; + row[State10.base_table_index()] = trace_row[10]; + row[State11.base_table_index()] = trace_row[11]; + row[State12.base_table_index()] = trace_row[12]; + row[State13.base_table_index()] = trace_row[13]; + row[State14.base_table_index()] = trace_row[14]; + row[State15.base_table_index()] = trace_row[15]; + + row[State0Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[0]); + row[State1Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[1]); + row[State2Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[2]); + row[State3Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[3]); + + let round_constants = Self::tip5_round_constants_by_round_number(round_number); + row[Constant0.base_table_index()] = round_constants[0]; + row[Constant1.base_table_index()] = round_constants[1]; + row[Constant2.base_table_index()] = round_constants[2]; + row[Constant3.base_table_index()] = round_constants[3]; + row[Constant4.base_table_index()] = round_constants[4]; + row[Constant5.base_table_index()] = round_constants[5]; + row[Constant6.base_table_index()] = round_constants[6]; + row[Constant7.base_table_index()] = round_constants[7]; + row[Constant8.base_table_index()] = round_constants[8]; + row[Constant9.base_table_index()] = round_constants[9]; + row[Constant10.base_table_index()] = round_constants[10]; + row[Constant11.base_table_index()] = round_constants[11]; + row[Constant12.base_table_index()] = round_constants[12]; + row[Constant13.base_table_index()] = round_constants[13]; + row[Constant14.base_table_index()] = round_constants[14]; + row[Constant15.base_table_index()] = round_constants[15]; } hash_trace_addendum } - /// The 2·STATE_SIZE (= NUM_ROUND_CONSTANTS) round constants for round `round_number`. - /// Of note: - /// - Round index 0 indicates a padding row – all constants are zero. - /// - Round index 9 indicates an output row – all constants are zero. - pub fn rescue_xlix_round_constants_by_round_number( + /// The round constants for round `round_number` if it is a valid round number in the Tip5 + /// permutation, and the zero vector otherwise. + fn tip5_round_constants_by_round_number( round_number: usize, ) -> [BFieldElement; NUM_ROUND_CONSTANTS] { match round_number { - i if i == 0 || i == NUM_ROUNDS + 1 => [BFIELD_ZERO; NUM_ROUND_CONSTANTS], - i if i <= NUM_ROUNDS => ROUND_CONSTANTS - [NUM_ROUND_CONSTANTS * (i - 1)..NUM_ROUND_CONSTANTS * i] + i if i < tip5::NUM_ROUNDS => tip5::ROUND_CONSTANTS + [NUM_ROUND_CONSTANTS * i..NUM_ROUND_CONSTANTS * (i + 1)] .try_into() .unwrap(), - _ => panic!("Round with number {round_number} does not have round constants."), + _ => [BFIELD_ZERO; NUM_ROUND_CONSTANTS], } } + + /// The inverse-or-zero of (`mid_high` + (`highest` << 16) - (1 << 32) + 1) where `highest` + /// is the most significant limb of the given `state_element`, and `mid_high` the second-most + /// significant limb. + fn inverse_or_zero_of_highest_2_limbs(state_element: BFieldElement) -> BFieldElement { + let limbs = state_element.raw_u16s().map(|limb| limb as u64); + let highest = limbs[0]; + let mid_high = limbs[1]; + let to_invert = mid_high + (highest << 16) - (1 << 32) + 1; + BFieldElement::from_raw_u64(to_invert).inverse_or_zero() + } } #[cfg(test)] From e322e135e44159c8871576d56c752ea993010739 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 10:23:44 +0100 Subject: [PATCH 08/49] rewrite hashing-related tests to use Tip5 --- triton-vm/src/vm.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 476fb79cc..b4ea72666 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -13,10 +13,10 @@ use num_traits::Zero; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ZERO; use twenty_first::shared_math::other::log_2_floor; -use twenty_first::shared_math::rescue_prime_digest::DIGEST_LENGTH; use twenty_first::shared_math::tip5; use twenty_first::shared_math::tip5::Tip5; use twenty_first::shared_math::tip5::Tip5State; +use twenty_first::shared_math::tip5::DIGEST_LENGTH; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; use twenty_first::util_types::algebraic_hasher::Domain; @@ -1184,8 +1184,8 @@ pub mod triton_vm_tests { use twenty_first::shared_math::other::log_2_floor; use twenty_first::shared_math::other::random_elements; use twenty_first::shared_math::other::random_elements_array; - use twenty_first::shared_math::rescue_prime_digest::Digest; - use twenty_first::shared_math::rescue_prime_regular::RescuePrimeRegular; + use twenty_first::shared_math::tip5::Digest; + use twenty_first::shared_math::tip5::Tip5; use twenty_first::shared_math::traits::FiniteField; use twenty_first::shared_math::traits::ModPowU32; use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; @@ -1353,7 +1353,7 @@ pub mod triton_vm_tests { hash_input[0] = BFieldElement::new(3); hash_input[1] = BFieldElement::new(2); hash_input[2] = BFieldElement::new(1); - let digest = RescuePrimeRegular::hash_10(&hash_input); + let digest = Tip5::hash_10(&hash_input); SourceCodeAndInput { source_code: source_code.to_string(), input: vec![digest.to_vec()[0]], @@ -1469,15 +1469,16 @@ pub mod triton_vm_tests { let sponge_input = [st0, st1, st2, st3, st4, st5, st6, st7, st8, st9].map(BFieldElement::new); - let mut sponge_state = RescuePrimeRegular::init(); - RescuePrimeRegular::absorb(&mut sponge_state, &sponge_input); - let sponge_output = RescuePrimeRegular::squeeze(&mut sponge_state); - RescuePrimeRegular::absorb(&mut sponge_state, &sponge_output); - RescuePrimeRegular::absorb(&mut sponge_state, &sponge_output); - let sponge_output = RescuePrimeRegular::squeeze(&mut sponge_state); - RescuePrimeRegular::absorb(&mut sponge_state, &sponge_output); - RescuePrimeRegular::squeeze(&mut sponge_state); - let sponge_output = RescuePrimeRegular::squeeze(&mut sponge_state); + + let mut sponge = Tip5::init(); + Tip5::absorb(&mut sponge, &sponge_input); + let sponge_output = Tip5::squeeze(&mut sponge); + Tip5::absorb(&mut sponge, &sponge_output); + Tip5::absorb(&mut sponge, &sponge_output); + let sponge_output = Tip5::squeeze(&mut sponge); + Tip5::absorb(&mut sponge, &sponge_output); + Tip5::squeeze(&mut sponge); + let sponge_output = Tip5::squeeze(&mut sponge); let source_code = format!( " @@ -2124,7 +2125,7 @@ pub mod triton_vm_tests { fn tvm_op_stack_big_enough_test() { assert!( DIGEST_LENGTH <= OP_STACK_REG_COUNT, - "The OpStack must be large enough to hold a single Rescue-Prime digest" + "The OpStack must be large enough to hold a single digest" ); } @@ -2371,7 +2372,7 @@ pub mod triton_vm_tests { #[test] fn run_tvm_mt_ap_verify_test() { // generate merkle tree - type H = RescuePrimeRegular; + type H = Tip5; const NUM_LEAFS: usize = 64; let leafs: [Digest; NUM_LEAFS] = random_elements_array(); From c066848b97523c8205e447980e21789b45e5e920 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 10:34:51 +0100 Subject: [PATCH 09/49] update part of Hash Table's AIR --- triton-vm/src/table/hash_table.rs | 570 ++++++++++++++++++++---------- 1 file changed, 388 insertions(+), 182 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 7a5acc006..77730c5fa 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -9,14 +9,13 @@ use strum::EnumCount; use triton_opcodes::instruction::Instruction; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; -use twenty_first::shared_math::rescue_prime_digest::DIGEST_LENGTH; -use twenty_first::shared_math::rescue_prime_regular::ALPHA; -use twenty_first::shared_math::rescue_prime_regular::MDS; -use twenty_first::shared_math::rescue_prime_regular::MDS_INV; -use twenty_first::shared_math::rescue_prime_regular::NUM_ROUNDS; -use twenty_first::shared_math::rescue_prime_regular::RATE; -use twenty_first::shared_math::rescue_prime_regular::ROUND_CONSTANTS; -use twenty_first::shared_math::rescue_prime_regular::STATE_SIZE; +use twenty_first::shared_math::tip5::DIGEST_LENGTH; +use twenty_first::shared_math::tip5::MDS_MATRIX_FIRST_COLUMN; +use twenty_first::shared_math::tip5::NUM_ROUNDS; +use twenty_first::shared_math::tip5::NUM_SPLIT_AND_LOOKUP; +use twenty_first::shared_math::tip5::RATE; +use twenty_first::shared_math::tip5::ROUND_CONSTANTS; +use twenty_first::shared_math::tip5::STATE_SIZE; use twenty_first::shared_math::x_field_element::XFieldElement; use crate::table::challenges::ChallengeId::*; @@ -43,8 +42,8 @@ pub const BASE_WIDTH: usize = HashBaseTableColumn::COUNT; pub const EXT_WIDTH: usize = HashExtTableColumn::COUNT; pub const FULL_WIDTH: usize = BASE_WIDTH + EXT_WIDTH; +pub const POWER_MAP_EXPONENT: u64 = 7; pub const NUM_ROUND_CONSTANTS: usize = STATE_SIZE; -pub const TOTAL_NUM_CONSTANTS: usize = NUM_ROUND_CONSTANTS * NUM_ROUNDS; #[derive(Debug, Clone)] pub struct HashTable {} @@ -74,11 +73,40 @@ impl ExtHashTable { let running_evaluation_hash_digest = ext_row(HashDigestRunningEvaluation); let running_evaluation_sponge = ext_row(SpongeRunningEvaluation); - let ci_is_hash = ci.clone() - constant(Instruction::Hash.opcode_b()); - let ci_is_absorb_init = ci - constant(Instruction::AbsorbInit.opcode_b()); + let two_pow_16 = constant(BFieldElement::new(1_u64 << 16)); + let two_pow_32 = constant(BFieldElement::new(1_u64 << 32)); + let two_pow_48 = constant(BFieldElement::new(1_u64 << 48)); + + let state_0 = base_row(State0HighestLkIn) * two_pow_48.clone() + + base_row(State0MidHighLkIn) * two_pow_32.clone() + + base_row(State0MidLowLkIn) * two_pow_16.clone() + + base_row(State0LowestLkIn); + let state_1 = base_row(State1HighestLkIn) * two_pow_48.clone() + + base_row(State1MidHighLkIn) * two_pow_32.clone() + + base_row(State1MidLowLkIn) * two_pow_16.clone() + + base_row(State1LowestLkIn); + let state_2 = base_row(State2HighestLkIn) * two_pow_48.clone() + + base_row(State2MidHighLkIn) * two_pow_32.clone() + + base_row(State2MidLowLkIn) * two_pow_16.clone() + + base_row(State2LowestLkIn); + let state_3 = base_row(State3HighestLkIn) * two_pow_48.clone() + + base_row(State3MidHighLkIn) * two_pow_32.clone() + + base_row(State3MidLowLkIn) * two_pow_16.clone() + + base_row(State3LowestLkIn); + let state = [ - STATE0, STATE1, STATE2, STATE3, STATE4, STATE5, STATE6, STATE7, STATE8, STATE9, + state_0, + state_1, + state_2, + state_3, + base_row(State4), + base_row(State5), + base_row(State6), + base_row(State7), + base_row(State8), + base_row(State9), ]; + let state_weights = [ HashStateWeight0, HashStateWeight1, @@ -94,14 +122,19 @@ impl ExtHashTable { let compressed_row: ConstraintCircuitMonad<_> = state_weights .into_iter() .zip_eq(state.into_iter()) - .map(|(weight, state)| challenge(weight) * base_row(state)) + .map(|(weight, state)| challenge(weight) * state) .sum(); - let round_number_is_0_or_1 = round_number.clone() * (round_number.clone() - one.clone()); + let round_number_is_neg_1_or_0 = + (round_number.clone() + one.clone()) * round_number.clone(); + let ci_is_hash = ci.clone() - constant(Instruction::Hash.opcode_b()); + let ci_is_absorb_init = ci - constant(Instruction::AbsorbInit.opcode_b()); let current_instruction_is_absorb_init_or_hash = ci_is_absorb_init.clone() * ci_is_hash.clone(); + // todo >>> here <<< + // Evaluation Argument “hash input” // If the round number is 0, the running evaluation is the default initial. // If the current instruction is AbsorbInit, the running evaluation is the default initial. @@ -135,7 +168,7 @@ impl ExtHashTable { + ci_is_absorb_init * running_evaluation_sponge_is_default_initial; let mut constraints = [ - round_number_is_0_or_1, + round_number_is_neg_1_or_0, current_instruction_is_absorb_init_or_hash, running_evaluation_hash_input_is_initialized_correctly, running_evaluation_hash_digest_is_default_initial, @@ -149,10 +182,10 @@ impl ExtHashTable { fn round_number_deselector( circuit_builder: &ConstraintCircuitBuilder, round_number_circuit_node: &ConstraintCircuitMonad, - round_number_to_deselect: usize, + round_number_to_deselect: isize, ) -> ConstraintCircuitMonad { let constant = |c: u64| circuit_builder.b_constant(c.into()); - (0..=NUM_ROUNDS + 1) + (-1..=NUM_ROUNDS as isize) .filter(|&r| r != round_number_to_deselect) .map(|r| round_number_circuit_node.clone() - constant(r as u64)) .fold(constant(1), |a, b| a * b) @@ -162,56 +195,97 @@ impl ExtHashTable { let circuit_builder = ConstraintCircuitBuilder::new(); let constant = |c: u64| circuit_builder.b_constant(c.into()); - let round_number = circuit_builder.input(BaseRow(ROUNDNUMBER.master_base_table_index())); + let round_number = circuit_builder.input(BaseRow(RoundNumber.master_base_table_index())); let ci = circuit_builder.input(BaseRow(CI.master_base_table_index())); - let state10 = circuit_builder.input(BaseRow(STATE10.master_base_table_index())); - let state11 = circuit_builder.input(BaseRow(STATE11.master_base_table_index())); - let state12 = circuit_builder.input(BaseRow(STATE12.master_base_table_index())); - let state13 = circuit_builder.input(BaseRow(STATE13.master_base_table_index())); - let state14 = circuit_builder.input(BaseRow(STATE14.master_base_table_index())); - let state15 = circuit_builder.input(BaseRow(STATE15.master_base_table_index())); + let state10 = circuit_builder.input(BaseRow(State10.master_base_table_index())); + let state11 = circuit_builder.input(BaseRow(State11.master_base_table_index())); + let state12 = circuit_builder.input(BaseRow(State12.master_base_table_index())); + let state13 = circuit_builder.input(BaseRow(State13.master_base_table_index())); + let state14 = circuit_builder.input(BaseRow(State14.master_base_table_index())); + let state15 = circuit_builder.input(BaseRow(State15.master_base_table_index())); let ci_is_hash = ci.clone() - constant(Instruction::Hash.opcode() as u64); let ci_is_absorb_init = ci.clone() - constant(Instruction::AbsorbInit.opcode() as u64); let ci_is_absorb = ci.clone() - constant(Instruction::Absorb.opcode() as u64); let ci_is_squeeze = ci - constant(Instruction::Squeeze.opcode() as u64); + let round_number_is_not_neg_1 = + Self::round_number_deselector(&circuit_builder, &round_number, -1); let round_number_is_not_0 = Self::round_number_deselector(&circuit_builder, &round_number, 0); - let round_number_is_not_1 = - Self::round_number_deselector(&circuit_builder, &round_number, 1); + + let if_padding_row_then_ci_is_hash = round_number_is_not_neg_1 * ci_is_hash.clone(); + + let if_ci_is_hash_and_round_no_is_0_then_ = round_number_is_not_0.clone() + * ci_is_absorb_init + * ci_is_absorb.clone() + * ci_is_squeeze.clone(); + let if_ci_is_hash_and_round_no_is_0_then_state_10_is_1 = + if_ci_is_hash_and_round_no_is_0_then_.clone() * (state10.clone() - constant(1)); + let if_ci_is_hash_and_round_no_is_0_then_state_11_is_1 = + if_ci_is_hash_and_round_no_is_0_then_.clone() * (state11.clone() - constant(1)); + let if_ci_is_hash_and_round_no_is_0_then_state_12_is_1 = + if_ci_is_hash_and_round_no_is_0_then_.clone() * (state12.clone() - constant(1)); + let if_ci_is_hash_and_round_no_is_0_then_state_13_is_1 = + if_ci_is_hash_and_round_no_is_0_then_.clone() * (state13.clone() - constant(1)); + let if_ci_is_hash_and_round_no_is_0_then_state_14_is_1 = + if_ci_is_hash_and_round_no_is_0_then_.clone() * (state14.clone() - constant(1)); + let if_ci_is_hash_and_round_no_is_0_then_state_15_is_1 = + if_ci_is_hash_and_round_no_is_0_then_.clone() * (state15.clone() - constant(1)); + + let if_ci_is_absorb_init_and_round_no_is_0_then_ = round_number_is_not_0.clone() + * ci_is_hash + * ci_is_absorb.clone() + * ci_is_squeeze.clone(); + let if_ci_is_absorb_init_and_round_no_is_0_then_state_10_is_0 = + if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state10; + let if_ci_is_absorb_init_and_round_no_is_0_then_state_11_is_0 = + if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state11; + let if_ci_is_absorb_init_and_round_no_is_0_then_state_12_is_0 = + if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state12; + let if_ci_is_absorb_init_and_round_no_is_0_then_state_13_is_0 = + if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state13; + let if_ci_is_absorb_init_and_round_no_is_0_then_state_14_is_0 = + if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state14; + let if_ci_is_absorb_init_and_round_no_is_0_then_state_15_is_0 = + if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state15; + let mut constraints = vec![ - round_number_is_not_0 * ci_is_hash.clone(), - round_number_is_not_1.clone() - * ci_is_absorb_init - * ci_is_absorb.clone() - * ci_is_squeeze.clone() - * (state10.clone() - constant(1)), - round_number_is_not_1.clone() - * ci_is_hash - * ci_is_absorb.clone() - * ci_is_squeeze.clone() - * state10, - round_number_is_not_1.clone() * ci_is_absorb.clone() * ci_is_squeeze.clone() * state11, - round_number_is_not_1.clone() * ci_is_absorb.clone() * ci_is_squeeze.clone() * state12, - round_number_is_not_1.clone() * ci_is_absorb.clone() * ci_is_squeeze.clone() * state13, - round_number_is_not_1.clone() * ci_is_absorb.clone() * ci_is_squeeze.clone() * state14, - round_number_is_not_1 * ci_is_absorb * ci_is_squeeze * state15, + if_padding_row_then_ci_is_hash, + if_ci_is_hash_and_round_no_is_0_then_state_10_is_1, + if_ci_is_hash_and_round_no_is_0_then_state_11_is_1, + if_ci_is_hash_and_round_no_is_0_then_state_12_is_1, + if_ci_is_hash_and_round_no_is_0_then_state_13_is_1, + if_ci_is_hash_and_round_no_is_0_then_state_14_is_1, + if_ci_is_hash_and_round_no_is_0_then_state_15_is_1, + if_ci_is_absorb_init_and_round_no_is_0_then_state_10_is_0, + if_ci_is_absorb_init_and_round_no_is_0_then_state_11_is_0, + if_ci_is_absorb_init_and_round_no_is_0_then_state_12_is_0, + if_ci_is_absorb_init_and_round_no_is_0_then_state_13_is_0, + if_ci_is_absorb_init_and_round_no_is_0_then_state_14_is_0, + if_ci_is_absorb_init_and_round_no_is_0_then_state_15_is_0, ]; - let round_constant_offset = CONSTANT0A.master_base_table_index(); - for round_constant_col_index in 0..NUM_ROUND_CONSTANTS { - let round_constant_input = - circuit_builder.input(BaseRow(round_constant_col_index + round_constant_offset)); - let round_constant_constraint_circuit = (1..=NUM_ROUNDS) - .map(|i| { - let round_constant_idx = - NUM_ROUND_CONSTANTS * (i - 1) + round_constant_col_index; - Self::round_number_deselector(&circuit_builder, &round_number, i) - * (round_constant_input.clone() - - circuit_builder.b_constant(ROUND_CONSTANTS[round_constant_idx])) - }) - .sum(); + for round_constant_column_idx in 0..NUM_ROUND_CONSTANTS { + let round_constant_column = + Self::round_constant_column_by_index(round_constant_column_idx); + let round_constant_column_circuit = + circuit_builder.input(BaseRow(round_constant_column.master_base_table_index())); + let mut round_constant_constraint_circuit = constant(0); + for round_idx in 0..NUM_ROUNDS { + let round_constant_idx_for_current_row = + NUM_ROUND_CONSTANTS * round_idx + round_constant_column_idx; + let round_constant_for_current_row = + circuit_builder.b_constant(ROUND_CONSTANTS[round_constant_idx_for_current_row]); + let round_deselector_circuit = Self::round_number_deselector( + &circuit_builder, + &round_number, + round_idx as isize, + ); + round_constant_constraint_circuit = round_constant_constraint_circuit + + round_deselector_circuit + * (round_constant_column_circuit.clone() - round_constant_for_current_row); + } constraints.push(round_constant_constraint_circuit); } @@ -222,6 +296,28 @@ impl ExtHashTable { .collect() } + fn round_constant_column_by_index(index: usize) -> HashBaseTableColumn { + match index { + 0 => Constant0, + 1 => Constant1, + 2 => Constant2, + 3 => Constant3, + 4 => Constant4, + 5 => Constant5, + 6 => Constant6, + 7 => Constant7, + 8 => Constant8, + 9 => Constant9, + 10 => Constant10, + 11 => Constant11, + 12 => Constant12, + 13 => Constant13, + 14 => Constant14, + 15 => Constant15, + _ => panic!("invalid constant column index"), + } + } + pub fn ext_transition_constraints_as_circuits() -> Vec> { let circuit_builder = ConstraintCircuitBuilder::new(); let challenge = |c| circuit_builder.challenge(c); @@ -250,24 +346,60 @@ impl ExtHashTable { let hash_digest_eval_indeterminate = challenge(HashDigestIndeterminate); let sponge_indeterminate = challenge(SpongeIndeterminate); - let round_number = current_base_row(ROUNDNUMBER); + let round_number = current_base_row(RoundNumber); let ci = current_base_row(CI); let running_evaluation_hash_input = current_ext_row(HashInputRunningEvaluation); let running_evaluation_hash_digest = current_ext_row(HashDigestRunningEvaluation); let running_evaluation_sponge = current_ext_row(SpongeRunningEvaluation); - let round_number_next = next_base_row(ROUNDNUMBER); + let round_number_next = next_base_row(RoundNumber); let ci_next = next_base_row(CI); let running_evaluation_hash_input_next = next_ext_row(HashInputRunningEvaluation); let running_evaluation_hash_digest_next = next_ext_row(HashDigestRunningEvaluation); let running_evaluation_sponge_next = next_ext_row(SpongeRunningEvaluation); - let state: [_; STATE_SIZE] = [ - STATE0, STATE1, STATE2, STATE3, STATE4, STATE5, STATE6, STATE7, STATE8, STATE9, - STATE10, STATE11, STATE12, STATE13, STATE14, STATE15, + let two_pow_16 = constant(1 << 16); + let two_pow_32 = constant(1 << 32); + let two_pow_48 = constant(1 << 48); + + let state_0 = current_base_row(State0HighestLkIn) * two_pow_48.clone() + + current_base_row(State0MidHighLkIn) * two_pow_32.clone() + + current_base_row(State0MidLowLkIn) * two_pow_16.clone() + + current_base_row(State0LowestLkIn); + let state_1 = current_base_row(State1HighestLkIn) * two_pow_48.clone() + + current_base_row(State1MidHighLkIn) * two_pow_32.clone() + + current_base_row(State1MidLowLkIn) * two_pow_16.clone() + + current_base_row(State1LowestLkIn); + let state_2 = current_base_row(State2HighestLkIn) * two_pow_48.clone() + + current_base_row(State2MidHighLkIn) * two_pow_32.clone() + + current_base_row(State2MidLowLkIn) * two_pow_16.clone() + + current_base_row(State2LowestLkIn); + let state_3 = current_base_row(State3HighestLkIn) * two_pow_48.clone() + + current_base_row(State3MidHighLkIn) * two_pow_32.clone() + + current_base_row(State3MidLowLkIn) * two_pow_16.clone() + + current_base_row(State3LowestLkIn); + + let state = [ + state_0, + state_1, + state_2, + state_3, + current_base_row(State4), + current_base_row(State5), + current_base_row(State6), + current_base_row(State7), + current_base_row(State8), + current_base_row(State9), + current_base_row(State10), + current_base_row(State11), + current_base_row(State12), + current_base_row(State13), + current_base_row(State14), + current_base_row(State15), ]; - let state_current = state.map(current_base_row); - let state_next = state.map(next_base_row); + + let (state_next, hash_function_round_correctly_performs_update) = + Self::tip5_constraints_as_circuits(&circuit_builder); let state_weights = [ HashStateWeight0, @@ -289,11 +421,12 @@ impl ExtHashTable { ] .map(challenge); - // round number + // todo >>> here <<<< + // round numbers evolve as - // 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9, and - // 9 -> 1 or 9 -> 0, and - // 0 -> 0 + // 0 -> 1 -> 2 -> 3 -> 4 -> 5, and + // 5 -> -1 or 5 -> 0, and + // -1 -> -1 let round_number_is_not_0 = Self::round_number_deselector(&circuit_builder, &round_number, 0); @@ -325,111 +458,6 @@ impl ExtHashTable { let if_round_number_is_not_9_then_ci_doesnt_change = (round_number.clone() - constant(NUM_ROUNDS as u64 + 1)) * (ci_next.clone() - ci); - // Rescue-XLIX - - let round_constants_a: [_; STATE_SIZE] = [ - CONSTANT0A, - CONSTANT1A, - CONSTANT2A, - CONSTANT3A, - CONSTANT4A, - CONSTANT5A, - CONSTANT6A, - CONSTANT7A, - CONSTANT8A, - CONSTANT9A, - CONSTANT10A, - CONSTANT11A, - CONSTANT12A, - CONSTANT13A, - CONSTANT14A, - CONSTANT15A, - ] - .map(current_base_row); - let round_constants_b: [_; STATE_SIZE] = [ - CONSTANT0B, - CONSTANT1B, - CONSTANT2B, - CONSTANT3B, - CONSTANT4B, - CONSTANT5B, - CONSTANT6B, - CONSTANT7B, - CONSTANT8B, - CONSTANT9B, - CONSTANT10B, - CONSTANT11B, - CONSTANT12B, - CONSTANT13B, - CONSTANT14B, - CONSTANT15B, - ] - .map(current_base_row); - - // left-hand-side, starting at current round and going forward - - let after_sbox = { - let mut exponentiation_accumulator = state_current.to_vec(); - for _ in 1..ALPHA { - for i in 0..exponentiation_accumulator.len() { - exponentiation_accumulator[i] = - exponentiation_accumulator[i].clone() * state_current[i].clone(); - } - } - exponentiation_accumulator - }; - let after_mds = (0..STATE_SIZE) - .map(|i| { - (0..STATE_SIZE) - .map(|j| b_constant(MDS[i * STATE_SIZE + j]) * after_sbox[j].clone()) - .sum::>() - }) - .collect_vec(); - - let after_constants = after_mds - .into_iter() - .zip_eq(round_constants_a) - .map(|(st, rndc)| st + rndc) - .collect_vec(); - - // right hand side; move backwards - let before_constants = state_next - .clone() - .into_iter() - .zip_eq(round_constants_b) - .map(|(st, rndc)| st - rndc) - .collect_vec(); - let before_mds = (0..STATE_SIZE) - .map(|i| { - (0..STATE_SIZE) - .map(|j| b_constant(MDS_INV[i * STATE_SIZE + j]) * before_constants[j].clone()) - .sum::>() - }) - .collect_vec(); - - let before_sbox = { - let mut exponentiation_accumulator = before_mds.clone(); - for _ in 1..ALPHA { - for i in 0..exponentiation_accumulator.len() { - exponentiation_accumulator[i] = - exponentiation_accumulator[i].clone() * before_mds[i].clone(); - } - } - exponentiation_accumulator - }; - - // Equate left hand side to right hand side. Ignore if padding row or after final round. - - let hash_function_round_correctly_performs_update = after_constants - .into_iter() - .zip_eq(before_sbox.into_iter()) - .map(|(lhs, rhs)| { - round_number.clone() - * (round_number.clone() - constant(NUM_ROUNDS as u64 + 1)) - * (lhs - rhs) - }) - .collect_vec(); - // copy capacity between rounds with index 9 and 1 if instruction is “absorb” let round_number_next_is_not_1 = Self::round_number_deselector(&circuit_builder, &round_number_next, 1); @@ -572,7 +600,7 @@ impl ExtHashTable { if_ci_is_hash_then_ci_doesnt_change, if_round_number_is_not_9_then_ci_doesnt_change, ], - hash_function_round_correctly_performs_update, + hash_function_round_correctly_performs_update.to_vec(), vec![ if_round_number_next_is_1_and_ci_next_is_absorb_then_capacity_doesnt_change, if_round_number_next_is_1_and_ci_next_is_squeeze_then_state_doesnt_change, @@ -590,6 +618,160 @@ impl ExtHashTable { .collect() } + fn tip5_constraints_as_circuits( + circuit_builder: &ConstraintCircuitBuilder, + ) -> ( + [ConstraintCircuitMonad; STATE_SIZE], + [ConstraintCircuitMonad; STATE_SIZE], + ) { + let constant = |c: u64| circuit_builder.b_constant(c.into()); + let b_constant = |c| circuit_builder.b_constant(c); + let current_base_row = |column_idx: HashBaseTableColumn| { + circuit_builder.input(CurrentBaseRow(column_idx.master_base_table_index())) + }; + let next_base_row = |column_idx: HashBaseTableColumn| { + circuit_builder.input(NextBaseRow(column_idx.master_base_table_index())) + }; + + let two_pow_16 = constant(1 << 16); + let two_pow_32 = constant(1 << 32); + let two_pow_48 = constant(1 << 48); + + let state_0_after_lookup = current_base_row(State0HighestLkOut) * two_pow_48.clone() + + current_base_row(State0MidHighLkOut) * two_pow_32.clone() + + current_base_row(State0MidLowLkOut) * two_pow_16.clone() + + current_base_row(State0LowestLkOut); + let state_1_after_lookup = current_base_row(State1HighestLkOut) * two_pow_48.clone() + + current_base_row(State1MidHighLkOut) * two_pow_32.clone() + + current_base_row(State1MidLowLkOut) * two_pow_16.clone() + + current_base_row(State1LowestLkOut); + let state_2_after_lookup = current_base_row(State2HighestLkOut) * two_pow_48.clone() + + current_base_row(State2MidHighLkOut) * two_pow_32.clone() + + current_base_row(State2MidLowLkOut) * two_pow_16.clone() + + current_base_row(State2LowestLkOut); + let state_3_after_lookup = current_base_row(State3HighestLkOut) * two_pow_48.clone() + + current_base_row(State3MidHighLkOut) * two_pow_32.clone() + + current_base_row(State3MidLowLkOut) * two_pow_16.clone() + + current_base_row(State3LowestLkOut); + + let state_part_before_power_map: [_; STATE_SIZE - NUM_SPLIT_AND_LOOKUP] = [ + State4, State5, State6, State7, State8, State9, State10, State11, State12, State13, + State14, State15, + ] + .map(current_base_row); + + let state_part_after_power_map = { + let mut exponentiation_accumulator = state_part_before_power_map.clone(); + for _ in 1..POWER_MAP_EXPONENT { + for (i, state) in exponentiation_accumulator.iter_mut().enumerate() { + *state = state.clone() * state_part_before_power_map[i].clone(); + } + } + exponentiation_accumulator + }; + + let state_after_s_box_application = [ + state_0_after_lookup, + state_1_after_lookup, + state_2_after_lookup, + state_3_after_lookup, + state_part_after_power_map[0].clone(), + state_part_after_power_map[1].clone(), + state_part_after_power_map[2].clone(), + state_part_after_power_map[3].clone(), + state_part_after_power_map[4].clone(), + state_part_after_power_map[5].clone(), + state_part_after_power_map[6].clone(), + state_part_after_power_map[7].clone(), + state_part_after_power_map[8].clone(), + state_part_after_power_map[9].clone(), + state_part_after_power_map[10].clone(), + state_part_after_power_map[11].clone(), + ]; + + let state_after_matrix_multiplication: [_; STATE_SIZE] = { + let mut result_vec = Vec::with_capacity(STATE_SIZE); + for row_idx in 0..STATE_SIZE { + let mut current_accumulator = constant(0); + for col_idx in 0..STATE_SIZE { + let mds_matrix_entry = + b_constant(HashTable::mds_matrix_entry(row_idx, col_idx)); + let state_entry = state_after_s_box_application[col_idx].clone(); + current_accumulator = current_accumulator + mds_matrix_entry * state_entry; + } + result_vec.push(current_accumulator); + } + result_vec.try_into().unwrap() + }; + + let round_constants: [_; STATE_SIZE] = [ + Constant0, Constant1, Constant2, Constant3, Constant4, Constant5, Constant6, Constant7, + Constant8, Constant9, Constant10, Constant11, Constant12, Constant13, Constant14, + Constant15, + ] + .map(current_base_row); + + let state_after_round_constant_addition: [_; STATE_SIZE] = + state_after_matrix_multiplication + .into_iter() + .zip_eq(round_constants) + .map(|(st, rndc)| st + rndc) + .collect_vec() + .try_into() + .unwrap(); + + let state_0_next = next_base_row(State0HighestLkIn) * two_pow_48.clone() + + next_base_row(State0MidHighLkIn) * two_pow_32.clone() + + next_base_row(State0MidLowLkIn) * two_pow_16.clone() + + next_base_row(State0LowestLkIn); + let state_1_next = next_base_row(State1HighestLkIn) * two_pow_48.clone() + + next_base_row(State1MidHighLkIn) * two_pow_32.clone() + + next_base_row(State1MidLowLkIn) * two_pow_16.clone() + + next_base_row(State1LowestLkIn); + let state_2_next = next_base_row(State2HighestLkIn) * two_pow_48.clone() + + next_base_row(State2MidHighLkIn) * two_pow_32.clone() + + next_base_row(State2MidLowLkIn) * two_pow_16.clone() + + next_base_row(State2LowestLkIn); + let state_3_next = next_base_row(State3HighestLkIn) * two_pow_48 + + next_base_row(State3MidHighLkIn) * two_pow_32 + + next_base_row(State3MidLowLkIn) * two_pow_16 + + next_base_row(State3LowestLkIn); + + let state_next = [ + state_0_next, + state_1_next, + state_2_next, + state_3_next, + next_base_row(State4), + next_base_row(State5), + next_base_row(State6), + next_base_row(State7), + next_base_row(State8), + next_base_row(State9), + next_base_row(State10), + next_base_row(State11), + next_base_row(State12), + next_base_row(State13), + next_base_row(State14), + next_base_row(State15), + ]; + + let round_number = current_base_row(RoundNumber); + let hash_function_round_correctly_performs_update = state_after_round_constant_addition + .into_iter() + .zip_eq(state_next.clone().into_iter()) + .map(|(state_element, state_element_next)| { + (round_number.clone() + constant(1)) + * (round_number.clone() - constant(NUM_ROUNDS as u64)) + * (state_element - state_element_next) + }) + .collect_vec() + .try_into() + .unwrap(); + + (state_next, hash_function_round_correctly_performs_update) + } + pub fn ext_terminal_constraints_as_circuits() -> Vec> { // no more constraints vec![] @@ -597,6 +779,15 @@ impl ExtHashTable { } impl HashTable { + /// Get the MDS matrix's entry in row `row_idx` and column `col_idx`. + pub const fn mds_matrix_entry(row_idx: usize, col_idx: usize) -> BFieldElement { + assert!(row_idx < STATE_SIZE); + assert!(col_idx < STATE_SIZE); + let index_in_matrix_defining_column = (row_idx - col_idx) % STATE_SIZE; + let mds_matrix_entry = MDS_MATRIX_FIRST_COLUMN[index_in_matrix_defining_column]; + BFieldElement::new(mds_matrix_entry as u64) + } + pub fn fill_trace( hash_table: &mut ArrayViewMut2, aet: &AlgebraicExecutionTrace, @@ -636,18 +827,33 @@ impl HashTable { let mut hash_digest_running_evaluation = EvalArg::default_initial(); let mut sponge_running_evaluation = EvalArg::default_initial(); + let two_pow_16 = BFieldElement::from(1_u64 << 16); + let two_pow_32 = BFieldElement::from(1_u64 << 32); + let two_pow_48 = BFieldElement::from(1_u64 << 48); let rate_registers = |row: ArrayView1| { [ - row[STATE0.base_table_index()], - row[STATE1.base_table_index()], - row[STATE2.base_table_index()], - row[STATE3.base_table_index()], - row[STATE4.base_table_index()], - row[STATE5.base_table_index()], - row[STATE6.base_table_index()], - row[STATE7.base_table_index()], - row[STATE8.base_table_index()], - row[STATE9.base_table_index()], + row[State0HighestLkIn.base_table_index()] * two_pow_48 + + row[State0MidHighLkIn.base_table_index()] * two_pow_32 + + row[State0MidLowLkIn.base_table_index()] * two_pow_16 + + row[State0LowestLkIn.base_table_index()], + row[State1HighestLkIn.base_table_index()] * two_pow_48 + + row[State1MidHighLkIn.base_table_index()] * two_pow_32 + + row[State1MidLowLkIn.base_table_index()] * two_pow_16 + + row[State1LowestLkIn.base_table_index()], + row[State2HighestLkIn.base_table_index()] * two_pow_48 + + row[State2MidHighLkIn.base_table_index()] * two_pow_32 + + row[State2MidLowLkIn.base_table_index()] * two_pow_16 + + row[State2LowestLkIn.base_table_index()], + row[State3HighestLkIn.base_table_index()] * two_pow_48 + + row[State3MidHighLkIn.base_table_index()] * two_pow_32 + + row[State3MidLowLkIn.base_table_index()] * two_pow_16 + + row[State3LowestLkIn.base_table_index()], + row[State4.base_table_index()], + row[State5.base_table_index()], + row[State6.base_table_index()], + row[State7.base_table_index()], + row[State8.base_table_index()], + row[State9.base_table_index()], ] }; let state_weights = [ From 8a87b956ae34d286c66daef883c0b1677ba2f593 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 10:36:28 +0100 Subject: [PATCH 10/49] correctly pad Hash Table --- triton-vm/src/table/hash_table.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 77730c5fa..dbf8da527 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -807,6 +807,9 @@ impl HashTable { hash_table .slice_mut(s![hash_table_length.., CI.base_table_index()]) .fill(Instruction::Hash.opcode_b()); + hash_table + .slice_mut(s![hash_table_length.., RoundNumber.base_table_index()]) + .fill(-BFIELD_ONE); } pub fn extend( From 48ddbe8fbb0d81c91d35f99c147bc298a8dbf0ec Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 10:59:10 +0100 Subject: [PATCH 11/49] Hash Table's initial constraints --- triton-vm/src/table/hash_table.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index dbf8da527..ff89f9d87 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -133,23 +133,22 @@ impl ExtHashTable { let current_instruction_is_absorb_init_or_hash = ci_is_absorb_init.clone() * ci_is_hash.clone(); - // todo >>> here <<< - // Evaluation Argument “hash input” - // If the round number is 0, the running evaluation is the default initial. + // If the round number is -1, the running evaluation is the default initial. // If the current instruction is AbsorbInit, the running evaluation is the default initial. // Else, the first update has been applied to the running evaluation. - let from_processor_indeterminate = challenge(HashInputIndeterminate); + let hash_input_indeterminate = challenge(HashInputIndeterminate); let running_evaluation_hash_input_is_default_initial = running_evaluation_hash_input.clone() - running_evaluation_initial.clone(); let running_evaluation_hash_input_has_accumulated_first_row = running_evaluation_hash_input - - running_evaluation_initial.clone() * from_processor_indeterminate + - running_evaluation_initial.clone() * hash_input_indeterminate - compressed_row.clone(); - let running_evaluation_hash_input_is_initialized_correctly = round_number.clone() + let running_evaluation_hash_input_is_initialized_correctly = (round_number.clone() + + one.clone()) * ci_is_absorb_init.clone() * running_evaluation_hash_input_has_accumulated_first_row + ci_is_hash.clone() * running_evaluation_hash_input_is_default_initial.clone() - + (one - round_number) * running_evaluation_hash_input_is_default_initial; + + round_number * running_evaluation_hash_input_is_default_initial; // Evaluation Argument “hash digest” let running_evaluation_hash_digest_is_default_initial = @@ -883,7 +882,7 @@ impl HashTable { let current_row = base_table.row(row_idx); let current_instruction = current_row[CI.base_table_index()]; - if current_row[ROUNDNUMBER.base_table_index()].value() == NUM_ROUNDS as u64 + 1 + if current_row[RoundNumber.base_table_index()].value() == NUM_ROUNDS as u64 + 1 && current_instruction == opcode_hash { // add compressed digest to running evaluation “hash digest” @@ -899,7 +898,7 @@ impl HashTable { } // all remaining Evaluation Arguments only get updated if the round number is 1 - if current_row[ROUNDNUMBER.base_table_index()].is_one() { + if current_row[RoundNumber.base_table_index()].is_one() { let elements_for_hash_input_and_sponge_operations = match current_instruction { op if op == opcode_hash || op == opcode_absorb_init || op == opcode_squeeze => { rate_registers(current_row) From 52f6f76e85108e4ecc481f490641972c30df0232 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 12:32:10 +0100 Subject: [PATCH 12/49] update Hash Table AIR --- triton-vm/src/table/hash_table.rs | 167 ++++++++++++++---------------- 1 file changed, 76 insertions(+), 91 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index ff89f9d87..a07fc0c86 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -183,6 +183,11 @@ impl ExtHashTable { round_number_circuit_node: &ConstraintCircuitMonad, round_number_to_deselect: isize, ) -> ConstraintCircuitMonad { + assert!( + -1 <= round_number_to_deselect && round_number_to_deselect <= NUM_ROUNDS as isize, + "Round number to deselect must be in the range [-1, {NUM_ROUNDS}] \ + but got {round_number_to_deselect}." + ); let constant = |c: u64| circuit_builder.b_constant(c.into()); (-1..=NUM_ROUNDS as isize) .filter(|&r| r != round_number_to_deselect) @@ -193,15 +198,18 @@ impl ExtHashTable { pub fn ext_consistency_constraints_as_circuits() -> Vec> { let circuit_builder = ConstraintCircuitBuilder::new(); let constant = |c: u64| circuit_builder.b_constant(c.into()); + let base_row = |column_id: HashBaseTableColumn| { + circuit_builder.input(BaseRow(column_id.master_base_table_index())) + }; - let round_number = circuit_builder.input(BaseRow(RoundNumber.master_base_table_index())); - let ci = circuit_builder.input(BaseRow(CI.master_base_table_index())); - let state10 = circuit_builder.input(BaseRow(State10.master_base_table_index())); - let state11 = circuit_builder.input(BaseRow(State11.master_base_table_index())); - let state12 = circuit_builder.input(BaseRow(State12.master_base_table_index())); - let state13 = circuit_builder.input(BaseRow(State13.master_base_table_index())); - let state14 = circuit_builder.input(BaseRow(State14.master_base_table_index())); - let state15 = circuit_builder.input(BaseRow(State15.master_base_table_index())); + let round_number = base_row(RoundNumber); + let ci = base_row(CI); + let state10 = base_row(State10); + let state11 = base_row(State11); + let state12 = base_row(State12); + let state13 = base_row(State13); + let state14 = base_row(State14); + let state15 = base_row(State15); let ci_is_hash = ci.clone() - constant(Instruction::Hash.opcode() as u64); let ci_is_absorb_init = ci.clone() - constant(Instruction::AbsorbInit.opcode() as u64); @@ -268,8 +276,7 @@ impl ExtHashTable { for round_constant_column_idx in 0..NUM_ROUND_CONSTANTS { let round_constant_column = Self::round_constant_column_by_index(round_constant_column_idx); - let round_constant_column_circuit = - circuit_builder.input(BaseRow(round_constant_column.master_base_table_index())); + let round_constant_column_circuit = base_row(round_constant_column); let mut round_constant_constraint_circuit = constant(0); for round_idx in 0..NUM_ROUNDS { let round_constant_idx_for_current_row = @@ -321,7 +328,6 @@ impl ExtHashTable { let circuit_builder = ConstraintCircuitBuilder::new(); let challenge = |c| circuit_builder.challenge(c); let constant = |c: u64| circuit_builder.b_constant(c.into()); - let b_constant = |c| circuit_builder.b_constant(c); let opcode_hash = constant(Instruction::Hash.opcode() as u64); let opcode_absorb_init = constant(Instruction::AbsorbInit.opcode() as u64); @@ -378,7 +384,7 @@ impl ExtHashTable { + current_base_row(State3MidLowLkIn) * two_pow_16.clone() + current_base_row(State3LowestLkIn); - let state = [ + let state_current = [ state_0, state_1, state_2, @@ -420,33 +426,26 @@ impl ExtHashTable { ] .map(challenge); - // todo >>> here <<<< - // round numbers evolve as // 0 -> 1 -> 2 -> 3 -> 4 -> 5, and // 5 -> -1 or 5 -> 0, and // -1 -> -1 - let round_number_is_not_0 = - Self::round_number_deselector(&circuit_builder, &round_number, 0); - let round_number_is_not_9 = - Self::round_number_deselector(&circuit_builder, &round_number, 9); - - // if round number is 0, then next round number is 0 - // DNF: rn in {1, ..., 9} ∨ rn* = 0 - let round_number_is_1_through_9_or_round_number_next_is_0 = - round_number_is_not_0 * round_number_next.clone(); - - // if round number is 9, then next round number is 0 or 1 - // DNF: rn =/= 9 ∨ rn* = 0 ∨ rn* = 1 - let round_number_is_0_through_8_or_round_number_next_is_0_or_1 = round_number_is_not_9 - * (constant(1) - round_number_next.clone()) - * round_number_next.clone(); - - // if round number is in {1, ..., 8} then next round number is +1 - // DNF: (rn == 0 ∨ rn == 9) ∨ rn* = rn + 1 - let round_number_is_0_or_9_or_increments_by_one = round_number.clone() - * (constant(NUM_ROUNDS as u64 + 1) - round_number.clone()) + let round_number_is_not_neg_1 = + Self::round_number_deselector(&circuit_builder, &round_number, -1); + let round_number_is_not_5 = + Self::round_number_deselector(&circuit_builder, &round_number, 5); + + let round_number_is_0_through_5_or_round_number_next_is_neg_1 = + round_number_is_not_neg_1 * (round_number_next.clone() + constant(1)); + + let round_number_is_neg_1_through_4_or_round_number_next_is_0_or_neg_1 = + round_number_is_not_5 + * round_number_next.clone() + * (round_number_next.clone() + constant(1)); + + let round_number_is_neg_1_or_5_or_increments_by_one = (round_number.clone() + constant(1)) + * (round_number.clone() - constant(NUM_ROUNDS as u64)) * (round_number_next.clone() - round_number.clone() - constant(1)); let if_ci_is_hash_then_ci_doesnt_change = (ci.clone() - opcode_absorb_init.clone()) @@ -454,13 +453,13 @@ impl ExtHashTable { * (ci.clone() - opcode_squeeze.clone()) * (ci_next.clone() - opcode_hash.clone()); - let if_round_number_is_not_9_then_ci_doesnt_change = - (round_number.clone() - constant(NUM_ROUNDS as u64 + 1)) * (ci_next.clone() - ci); + let if_round_number_is_not_5_then_ci_doesnt_change = + (round_number.clone() - constant(NUM_ROUNDS as u64)) * (ci_next.clone() - ci); - // copy capacity between rounds with index 9 and 1 if instruction is “absorb” - let round_number_next_is_not_1 = - Self::round_number_deselector(&circuit_builder, &round_number_next, 1); - let round_number_next_is_1 = round_number_next.clone() - constant(1); + // copy capacity between rounds with index 5 and 0 if instruction is “absorb” + let round_number_next_is_not_0 = + Self::round_number_deselector(&circuit_builder, &round_number_next, 0); + let round_number_next_is_0 = round_number_next.clone(); let difference_of_capacity_registers = state_current[RATE..] .iter() @@ -472,14 +471,14 @@ impl ExtHashTable { .zip_eq(difference_of_capacity_registers) .map(|(weight, state_difference)| weight.clone() * state_difference) .sum(); - let if_round_number_next_is_1_and_ci_next_is_absorb_then_capacity_doesnt_change = - round_number_next_is_not_1.clone() + let if_round_number_next_is_0_and_ci_next_is_absorb_then_capacity_doesnt_change = + round_number_next_is_not_0.clone() * (ci_next.clone() - opcode_hash.clone()) * (ci_next.clone() - opcode_absorb_init.clone()) * (ci_next.clone() - opcode_squeeze.clone()) * randomized_sum_of_capacity_differences; - // copy entire state between rounds with index 9 and 1 if instruction is “squeeze” + // copy entire state between rounds with index 5 and 0 if instruction is “squeeze” let difference_of_state_registers = state_current .iter() .zip_eq(state_next.iter()) @@ -490,8 +489,8 @@ impl ExtHashTable { .zip_eq(difference_of_state_registers.iter()) .map(|(weight, state_difference)| weight.clone() * state_difference.clone()) .sum(); - let if_round_number_next_is_1_and_ci_next_is_squeeze_then_state_doesnt_change = - round_number_next_is_not_1.clone() + let if_round_number_next_is_0_and_ci_next_is_squeeze_then_state_doesnt_change = + round_number_next_is_not_0.clone() * (ci_next.clone() - opcode_hash.clone()) * (ci_next.clone() - opcode_absorb_init.clone()) * (ci_next.clone() - opcode_absorb.clone()) @@ -499,47 +498,52 @@ impl ExtHashTable { // Evaluation Arguments - // If (and only if) the next row number is 1, update running evaluation “hash input.” + // If (and only if) the row number in the next row is 0 and the current instruction in + // the next row is corresponds to `hash`, update running evaluation “hash input.” let ci_next_is_not_hash = (ci_next.clone() - opcode_absorb_init.clone()) * (ci_next.clone() - opcode_absorb.clone()) * (ci_next.clone() - opcode_squeeze.clone()); let running_evaluation_hash_input_remains = running_evaluation_hash_input_next.clone() - running_evaluation_hash_input.clone(); - let tip5_input = state_next[0..2 * DIGEST_LENGTH].to_owned(); + let tip5_input = state_next[..RATE].to_owned(); let compressed_row_from_processor = tip5_input .into_iter() - .zip_eq(state_weights[0..2 * DIGEST_LENGTH].iter()) + .zip_eq(state_weights[..RATE].iter()) .map(|(state, weight)| weight.clone() * state) .sum(); let running_evaluation_hash_input_updates = running_evaluation_hash_input_next - hash_input_eval_indeterminate * running_evaluation_hash_input - compressed_row_from_processor; - let running_evaluation_hash_input_is_updated_correctly = round_number_next_is_not_1.clone() + let running_evaluation_hash_input_is_updated_correctly = round_number_next_is_not_0.clone() * ci_next_is_not_hash.clone() * running_evaluation_hash_input_updates - + round_number_next_is_1.clone() * running_evaluation_hash_input_remains.clone() + + round_number_next_is_0.clone() * running_evaluation_hash_input_remains.clone() + (ci_next.clone() - opcode_hash.clone()) * running_evaluation_hash_input_remains; - // If (and only if) the next row number is 9, update running evaluation “hash digest.” - let round_number_next_is_9 = round_number_next.clone() - constant(NUM_ROUNDS as u64 + 1); - let round_number_next_is_not_9 = - Self::round_number_deselector(&circuit_builder, &round_number_next, NUM_ROUNDS + 1); + // If (and only if) the row number in the next row is 5 and the current instruction in + // the next row corresponds to `hash`, update running evaluation “hash digest.” + let round_number_next_is_5 = round_number_next.clone() - constant(NUM_ROUNDS as u64); + let round_number_next_is_not_5 = Self::round_number_deselector( + &circuit_builder, + &round_number_next, + NUM_ROUNDS as isize, + ); let running_evaluation_hash_digest_remains = running_evaluation_hash_digest_next.clone() - running_evaluation_hash_digest.clone(); - let hash_digest = state_next[0..DIGEST_LENGTH].to_owned(); + let hash_digest = state_next[..DIGEST_LENGTH].to_owned(); let compressed_row_hash_digest = hash_digest .into_iter() - .zip_eq(state_weights[0..DIGEST_LENGTH].iter()) + .zip_eq(state_weights[..DIGEST_LENGTH].iter()) .map(|(state, weight)| weight.clone() * state) .sum(); let running_evaluation_hash_digest_updates = running_evaluation_hash_digest_next - hash_digest_eval_indeterminate * running_evaluation_hash_digest - compressed_row_hash_digest; - let running_evaluation_hash_digest_is_updated_correctly = round_number_next_is_not_9 + let running_evaluation_hash_digest_is_updated_correctly = round_number_next_is_not_5 * ci_next_is_not_hash * running_evaluation_hash_digest_updates - + round_number_next_is_9 * running_evaluation_hash_digest_remains.clone() + + round_number_next_is_5 * running_evaluation_hash_digest_remains.clone() + (ci_next.clone() - opcode_hash.clone()) * running_evaluation_hash_digest_remains; // The running evaluation for “Sponge” updates correctly. @@ -553,59 +557,40 @@ impl ExtHashTable { - sponge_indeterminate.clone() * running_evaluation_sponge.clone() - challenge(HashCIWeight) * ci_next.clone() - compressed_row_next; - let if_round_no_next_1_and_ci_next_absorb_init_or_squeeze_then_running_eval_sponge_updates = - round_number_next_is_not_1.clone() + let if_round_no_next_0_and_ci_next_is_spongy_then_running_eval_sponge_updates = + round_number_next_is_not_0.clone() * (ci_next.clone() - opcode_hash.clone()) - * (ci_next.clone() - opcode_absorb.clone()) * running_evaluation_sponge_has_accumulated_next_row; - let compressed_difference_of_rows: ConstraintCircuitMonad<_> = state_weights[..RATE] - .iter() - .zip_eq(difference_of_state_registers[..RATE].iter()) - .map(|(weight, state_difference)| weight.clone() * state_difference.clone()) - .sum(); - let running_evaluation_sponge_absorb_has_accumulated_difference_of_rows = - running_evaluation_sponge_next.clone() - - sponge_indeterminate * running_evaluation_sponge.clone() - - challenge(HashCIWeight) * ci_next.clone() - - compressed_difference_of_rows; - let if_round_no_next_is_1_and_ci_next_is_absorb_then_sponge_absorb_eval_is_updated = - round_number_next_is_not_1 - * (ci_next.clone() - opcode_hash) - * (ci_next.clone() - opcode_absorb_init.clone()) - * (ci_next.clone() - opcode_squeeze.clone()) - * running_evaluation_sponge_absorb_has_accumulated_difference_of_rows; - let running_evaluation_sponge_absorb_remains = running_evaluation_sponge_next - running_evaluation_sponge; - let if_round_no_next_is_not_1_then_running_evaluation_sponge_absorb_remains = - round_number_next_is_1 * running_evaluation_sponge_absorb_remains.clone(); + let if_round_no_next_is_not_0_then_running_evaluation_sponge_absorb_remains = + round_number_next_is_0 * running_evaluation_sponge_absorb_remains.clone(); let if_ci_next_is_not_spongy_then_running_evaluation_sponge_absorb_remains = (ci_next.clone() - opcode_absorb_init) * (ci_next.clone() - opcode_absorb) * (ci_next - opcode_squeeze) * running_evaluation_sponge_absorb_remains; - let running_evaluation_sponge_absorb_is_updated_correctly = - if_round_no_next_1_and_ci_next_absorb_init_or_squeeze_then_running_eval_sponge_updates - + if_round_no_next_is_1_and_ci_next_is_absorb_then_sponge_absorb_eval_is_updated - + if_round_no_next_is_not_1_then_running_evaluation_sponge_absorb_remains + let running_evaluation_sponge_is_updated_correctly = + if_round_no_next_0_and_ci_next_is_spongy_then_running_eval_sponge_updates + + if_round_no_next_is_not_0_then_running_evaluation_sponge_absorb_remains + if_ci_next_is_not_spongy_then_running_evaluation_sponge_absorb_remains; let mut constraints = [ vec![ - round_number_is_1_through_9_or_round_number_next_is_0, - round_number_is_0_through_8_or_round_number_next_is_0_or_1, - round_number_is_0_or_9_or_increments_by_one, + round_number_is_0_through_5_or_round_number_next_is_neg_1, + round_number_is_neg_1_through_4_or_round_number_next_is_0_or_neg_1, + round_number_is_neg_1_or_5_or_increments_by_one, if_ci_is_hash_then_ci_doesnt_change, - if_round_number_is_not_9_then_ci_doesnt_change, + if_round_number_is_not_5_then_ci_doesnt_change, ], hash_function_round_correctly_performs_update.to_vec(), vec![ - if_round_number_next_is_1_and_ci_next_is_absorb_then_capacity_doesnt_change, - if_round_number_next_is_1_and_ci_next_is_squeeze_then_state_doesnt_change, + if_round_number_next_is_0_and_ci_next_is_absorb_then_capacity_doesnt_change, + if_round_number_next_is_0_and_ci_next_is_squeeze_then_state_doesnt_change, running_evaluation_hash_input_is_updated_correctly, running_evaluation_hash_digest_is_updated_correctly, - running_evaluation_sponge_absorb_is_updated_correctly, + running_evaluation_sponge_is_updated_correctly, ], ] .concat(); From dfd27b6e42fe42f91792b7578ff89309167130fa Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 12:53:53 +0100 Subject: [PATCH 13/49] update Hash Table's `extend()` --- triton-vm/src/table/hash_table.rs | 33 ++++++++----------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index a07fc0c86..976469505 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -4,7 +4,7 @@ use ndarray::Array1; use ndarray::ArrayView1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; -use num_traits::One; +use num_traits::Zero; use strum::EnumCount; use triton_opcodes::instruction::Instruction; use twenty_first::shared_math::b_field_element::BFieldElement; @@ -867,7 +867,7 @@ impl HashTable { let current_row = base_table.row(row_idx); let current_instruction = current_row[CI.base_table_index()]; - if current_row[RoundNumber.base_table_index()].value() == NUM_ROUNDS as u64 + 1 + if current_row[RoundNumber.base_table_index()].value() == NUM_ROUNDS as u64 && current_instruction == opcode_hash { // add compressed digest to running evaluation “hash digest” @@ -882,28 +882,11 @@ impl HashTable { + compressed_hash_digest; } - // all remaining Evaluation Arguments only get updated if the round number is 1 - if current_row[RoundNumber.base_table_index()].is_one() { - let elements_for_hash_input_and_sponge_operations = match current_instruction { - op if op == opcode_hash || op == opcode_absorb_init || op == opcode_squeeze => { - rate_registers(current_row) - } - op if op == opcode_absorb => { - let rate_previous_row = rate_registers(previous_row); - let rate_current_row = rate_registers(current_row); - rate_current_row - .iter() - .zip_eq(rate_previous_row.iter()) - .map(|(¤t_state, &previous_state)| current_state - previous_state) - .collect_vec() - .try_into() - .unwrap() - } - _ => panic!("Opcode must be of `hash`, `absorb_init`, `absorb`, or `squeeze`."), - }; - let compressed_row_hash_input_and_sponge_operations: XFieldElement = state_weights + // all remaining Evaluation Arguments only get updated if the round number is 0 + if current_row[RoundNumber.base_table_index()].is_zero() { + let compressed_row: XFieldElement = state_weights .iter() - .zip_eq(elements_for_hash_input_and_sponge_operations.iter()) + .zip_eq(rate_registers(current_row).iter()) .map(|(&weight, &element)| weight * element) .sum(); @@ -911,7 +894,7 @@ impl HashTable { ci if ci == opcode_hash => { hash_input_running_evaluation = hash_input_running_evaluation * hash_input_eval_indeterminate - + compressed_row_hash_input_and_sponge_operations; + + compressed_row; } ci if ci == opcode_absorb_init || ci == opcode_absorb @@ -920,7 +903,7 @@ impl HashTable { sponge_running_evaluation = sponge_running_evaluation * sponge_eval_indeterminate + ci_weight * ci - + compressed_row_hash_input_and_sponge_operations; + + compressed_row; } _ => panic!("Opcode must be of `hash`, `absorb_init`, `absorb`, or `squeeze`."), } From 42b2136429ac10dbf0e7b5a2f8e30974329db3b2 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 14:11:29 +0100 Subject: [PATCH 14/49] =?UTF-8?q?remove=20superfluous=20=E2=80=9Cprevious?= =?UTF-8?q?=20row=E2=80=9D=20when=20extending=20the=20Hash=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- triton-vm/src/table/hash_table.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 976469505..ce4432967 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -861,18 +861,15 @@ impl HashTable { let opcode_absorb = Instruction::Absorb.opcode_b(); let opcode_squeeze = Instruction::Squeeze.opcode_b(); - let previous_row = Array1::zeros([BASE_WIDTH]); - let mut previous_row = previous_row.view(); for row_idx in 0..base_table.nrows() { - let current_row = base_table.row(row_idx); - let current_instruction = current_row[CI.base_table_index()]; + let row = base_table.row(row_idx); + let current_instruction = row[CI.base_table_index()]; - if current_row[RoundNumber.base_table_index()].value() == NUM_ROUNDS as u64 + if row[RoundNumber.base_table_index()].value() == NUM_ROUNDS as u64 && current_instruction == opcode_hash { // add compressed digest to running evaluation “hash digest” - let compressed_hash_digest: XFieldElement = rate_registers(current_row) - [..DIGEST_LENGTH] + let compressed_hash_digest: XFieldElement = rate_registers(row)[..DIGEST_LENGTH] .iter() .zip_eq(state_weights[..DIGEST_LENGTH].iter()) .map(|(&state, &weight)| weight * state) @@ -883,10 +880,10 @@ impl HashTable { } // all remaining Evaluation Arguments only get updated if the round number is 0 - if current_row[RoundNumber.base_table_index()].is_zero() { + if row[RoundNumber.base_table_index()].is_zero() { let compressed_row: XFieldElement = state_weights .iter() - .zip_eq(rate_registers(current_row).iter()) + .zip_eq(rate_registers(row).iter()) .map(|(&weight, &element)| weight * element) .sum(); @@ -915,8 +912,6 @@ impl HashTable { extension_row[HashDigestRunningEvaluation.ext_table_index()] = hash_digest_running_evaluation; extension_row[SpongeRunningEvaluation.ext_table_index()] = sponge_running_evaluation; - - previous_row = current_row; } } } From 320137278d5996e2ab2234c431b556e432c01949 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 14:45:54 +0100 Subject: [PATCH 15/49] extend the Lookup Table --- triton-vm/src/table/challenges.rs | 13 +++++++++ triton-vm/src/table/lookup_table.rs | 42 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 4b35b8c26..68dc0cfd8 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -123,6 +123,19 @@ pub enum ChallengeId { HashStateWeight14, HashStateWeight15, + /// The indeterminate for the Lookup Argument between the Cascade Table and the Lookup Table. + CascadeLookupIndeterminate, + + /// A weight for non-linearly combining multiple elements. Applies to + /// - `*LkIn` in the Cascade Table, and + /// - `LookIn` in the Lookup Table. + LookupTableInputWeight, + + /// A weight for non-linearly combining multiple elements. Applies to + /// - `*LkOut` in the Cascade Table, and + /// - `LookOut` in the Lookup Table. + LookupTableOutputWeight, + /// The indeterminate for the public Evaluation Argument establishing correctness of the /// Lookup Table. LookupTablePublicIndeterminate, diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index 0024044c7..2440e908f 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -2,19 +2,29 @@ use ndarray::s; use ndarray::Array1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; +use num_traits::Zero; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; use twenty_first::shared_math::tip5; +use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; +use crate::table::challenges::ChallengeId::CascadeLookupIndeterminate; +use crate::table::challenges::ChallengeId::LookupTableInputWeight; +use crate::table::challenges::ChallengeId::LookupTableOutputWeight; +use crate::table::challenges::ChallengeId::LookupTablePublicIndeterminate; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; use crate::table::constraint_circuit::DualRowIndicator; use crate::table::constraint_circuit::SingleRowIndicator; +use crate::table::cross_table_argument::CrossTableArg; +use crate::table::cross_table_argument::EvalArg; +use crate::table::cross_table_argument::LookupArg; use crate::table::table_column::LookupBaseTableColumn; use crate::table::table_column::LookupBaseTableColumn::*; use crate::table::table_column::LookupExtTableColumn; +use crate::table::table_column::LookupExtTableColumn::*; use crate::table::table_column::MasterBaseTableColumn; use crate::vm::AlgebraicExecutionTrace; @@ -69,6 +79,38 @@ impl LookupTable { assert_eq!(BASE_WIDTH, base_table.ncols()); assert_eq!(EXT_WIDTH, ext_table.ncols()); assert_eq!(base_table.nrows(), ext_table.nrows()); + + let look_in_weight = challenges.get_challenge(LookupTableInputWeight); + let look_out_weight = challenges.get_challenge(LookupTableOutputWeight); + let cascade_indeterminate = challenges.get_challenge(CascadeLookupIndeterminate); + let public_indeterminate = challenges.get_challenge(LookupTablePublicIndeterminate); + + let mut cascade_table_running_sum_log_derivative = LookupArg::default_initial(); + let mut public_running_evaluation = EvalArg::default_initial(); + + for row_idx in 0..base_table.nrows() { + let base_row = base_table.row(row_idx); + + let lookup_input = base_row[LookIn.base_table_index()]; + let lookup_output = base_row[LookOut.base_table_index()]; + let lookup_multiplicity = base_row[LookupMultiplicity.base_table_index()]; + let is_padding = base_row[IsPadding.base_table_index()]; + + if is_padding.is_zero() { + let compressed_row = + lookup_input * look_in_weight + lookup_output * look_out_weight; + cascade_table_running_sum_log_derivative += + (cascade_indeterminate - compressed_row).inverse() * lookup_multiplicity; + + public_running_evaluation = + public_running_evaluation * public_indeterminate + lookup_output; + } + + let mut ext_row = ext_table.row_mut(row_idx); + ext_row[CascadeTableServerLogDerivative.ext_table_index()] = + cascade_table_running_sum_log_derivative; + ext_row[PublicEvaluationArgument.ext_table_index()] = public_running_evaluation; + } } } From 65fa0df0e855459e80cfeb999f27323a4d9ca60c Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 18:32:00 +0100 Subject: [PATCH 16/49] add AIR for Lookup Table --- triton-vm/src/table/lookup_table.rs | 220 ++++++++++++++++++++++++++-- triton-vm/src/table/table_column.rs | 8 +- 2 files changed, 209 insertions(+), 19 deletions(-) diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index 2440e908f..f2fea97fc 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -2,22 +2,26 @@ use ndarray::s; use ndarray::Array1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; -use num_traits::Zero; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; +use twenty_first::shared_math::b_field_element::BFIELD_ZERO; use twenty_first::shared_math::tip5; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; +use crate::table::challenges::ChallengeId; use crate::table::challenges::ChallengeId::CascadeLookupIndeterminate; use crate::table::challenges::ChallengeId::LookupTableInputWeight; use crate::table::challenges::ChallengeId::LookupTableOutputWeight; use crate::table::challenges::ChallengeId::LookupTablePublicIndeterminate; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; +use crate::table::constraint_circuit::ConstraintCircuitBuilder; use crate::table::constraint_circuit::DualRowIndicator; +use crate::table::constraint_circuit::DualRowIndicator::*; use crate::table::constraint_circuit::SingleRowIndicator; +use crate::table::constraint_circuit::SingleRowIndicator::*; use crate::table::cross_table_argument::CrossTableArg; use crate::table::cross_table_argument::EvalArg; use crate::table::cross_table_argument::LookupArg; @@ -26,6 +30,7 @@ use crate::table::table_column::LookupBaseTableColumn::*; use crate::table::table_column::LookupExtTableColumn; use crate::table::table_column::LookupExtTableColumn::*; use crate::table::table_column::MasterBaseTableColumn; +use crate::table::table_column::MasterExtTableColumn; use crate::vm::AlgebraicExecutionTrace; pub const BASE_WIDTH: usize = LookupBaseTableColumn::COUNT; @@ -43,21 +48,38 @@ impl LookupTable { ) { assert!(lookup_table.nrows() >= 1 << 8); + // Lookup Table input let lookup_input = Array1::from_iter((0_u64..1 << 8).map(BFieldElement::new)); + let lookup_input_column = + lookup_table.slice_mut(s![..1_usize << 8, LookIn.base_table_index()]); + lookup_input.move_into(lookup_input_column); + + // Lookup Table output let lookup_output = Array1::from_iter( (0..1 << 8).map(|i| BFieldElement::new(tip5::LOOKUP_TABLE[i] as u64)), ); + let lookup_output_column = + lookup_table.slice_mut(s![..1_usize << 8, LookOut.base_table_index()]); + lookup_output.move_into(lookup_output_column); + + // Inverse of 2^8 - Lookup Table input + let inverse_of_2_pow_8_minus_lookup_input = Array1::from_iter( + (1_u64..(1 << 8) + 1) + .map(|a| BFieldElement::new(a).inverse()) + .rev(), + ); + let inverse_of_2_pow_8_minus_lookup_input_column = lookup_table.slice_mut(s![ + ..1_usize << 8, + InverseOf2Pow8MinusLookIn.base_table_index() + ]); + inverse_of_2_pow_8_minus_lookup_input + .move_into(inverse_of_2_pow_8_minus_lookup_input_column); + + // Lookup Table multiplicities let lookup_multiplicities = Array1::from_iter( aet.lookup_table_lookup_multiplicities .map(BFieldElement::new), ); - - let lookup_input_column = - lookup_table.slice_mut(s![..1_usize << 8, LookIn.base_table_index()]); - lookup_input.move_into(lookup_input_column); - let lookup_output_column = - lookup_table.slice_mut(s![..1_usize << 8, LookOut.base_table_index()]); - lookup_output.move_into(lookup_output_column); let lookup_multiplicities_column = lookup_table.slice_mut(s![..1_usize << 8, LookupMultiplicity.base_table_index()]); lookup_multiplicities.move_into(lookup_multiplicities_column); @@ -67,8 +89,8 @@ impl LookupTable { // The Lookup Table is always fully populated. let lookup_table_length: usize = 1 << 8; lookup_table - .slice_mut(s![lookup_table_length.., IsPadding.base_table_index()]) - .fill(BFIELD_ONE); + .slice_mut(s![lookup_table_length.., LookIn.base_table_index()]) + .fill(BFieldElement::new(1 << 8)); } pub fn extend( @@ -94,9 +116,9 @@ impl LookupTable { let lookup_input = base_row[LookIn.base_table_index()]; let lookup_output = base_row[LookOut.base_table_index()]; let lookup_multiplicity = base_row[LookupMultiplicity.base_table_index()]; - let is_padding = base_row[IsPadding.base_table_index()]; + let is_padding = base_row[InverseOf2Pow8MinusLookIn.base_table_index()] == BFIELD_ZERO; - if is_padding.is_zero() { + if is_padding { let compressed_row = lookup_input * look_in_weight + lookup_output * look_out_weight; cascade_table_running_sum_log_derivative += @@ -116,18 +138,184 @@ impl LookupTable { impl ExtLookupTable { pub fn ext_initial_constraints_as_circuits() -> Vec> { - vec![] + let circuit_builder = ConstraintCircuitBuilder::new(); + + let base_row = |col_id: LookupBaseTableColumn| { + circuit_builder.input(BaseRow(col_id.master_base_table_index())) + }; + let ext_row = |col_id: LookupExtTableColumn| { + circuit_builder.input(ExtRow(col_id.master_ext_table_index())) + }; + let challenge = |challenge_id: ChallengeId| circuit_builder.challenge(challenge_id); + + let lookup_input = base_row(LookIn); + let lookup_output = base_row(LookOut); + let lookup_multiplicity = base_row(LookupMultiplicity); + let cascade_table_server_log_derivative = ext_row(CascadeTableServerLogDerivative); + let public_evaluation_argument = ext_row(PublicEvaluationArgument); + + let lookup_input_is_0 = lookup_input.clone(); + + // Lookup Argument with Cascade Table + let lookup_argument_default_initial = + circuit_builder.x_constant(LookupArg::default_initial()); + let cascade_table_indeterminate = challenge(CascadeLookupIndeterminate); + let compressed_row = lookup_input * challenge(LookupTableInputWeight) + + lookup_output.clone() * challenge(LookupTableOutputWeight); + let cascade_table_log_derivative_is_initialized_correctly = + (cascade_table_server_log_derivative - lookup_argument_default_initial) + * (cascade_table_indeterminate - compressed_row) + - lookup_multiplicity; + + // public Evaluation Argument + let eval_argument_default_initial = circuit_builder.x_constant(EvalArg::default_initial()); + let public_indeterminate = challenge(LookupTablePublicIndeterminate); + let public_evaluation_argument_is_initialized_correctly = public_evaluation_argument + - eval_argument_default_initial * public_indeterminate + - lookup_output; + + [ + lookup_input_is_0, + cascade_table_log_derivative_is_initialized_correctly, + public_evaluation_argument_is_initialized_correctly, + ] + .map(|circuit| circuit.consume()) + .to_vec() } pub fn ext_consistency_constraints_as_circuits() -> Vec> { - vec![] + let circuit_builder = ConstraintCircuitBuilder::new(); + + let one = circuit_builder.b_constant(BFIELD_ONE); + let two_pow_8 = circuit_builder.b_constant(BFieldElement::new(1 << 8)); + + let base_row = |col_id: LookupBaseTableColumn| { + circuit_builder.input(BaseRow(col_id.master_base_table_index())) + }; + + let lookup_input = base_row(LookIn); + let inverse_of_2_pow_8_minus_lookup_input = base_row(InverseOf2Pow8MinusLookIn); + let lookup_multiplicity = base_row(LookupMultiplicity); + + let two_pow_8_minus_lookup_input = two_pow_8 - lookup_input; + let two_pow_8_minus_lookup_input_times_inv_is_1 = two_pow_8_minus_lookup_input.clone() + * inverse_of_2_pow_8_minus_lookup_input.clone() + - one; + let two_pow_8_minus_lookup_input_times_inv_is_1_or_lookup_input_is_two_pow_8 = + two_pow_8_minus_lookup_input_times_inv_is_1.clone() * two_pow_8_minus_lookup_input; + let two_pow_8_minus_lookup_input_times_inv_is_1_or_inv_is_zero = + two_pow_8_minus_lookup_input_times_inv_is_1.clone() + * inverse_of_2_pow_8_minus_lookup_input; + let if_lookup_input_is_256_then_multiplicity_is_0 = + two_pow_8_minus_lookup_input_times_inv_is_1 * lookup_multiplicity; + + [ + two_pow_8_minus_lookup_input_times_inv_is_1_or_lookup_input_is_two_pow_8, + two_pow_8_minus_lookup_input_times_inv_is_1_or_inv_is_zero, + if_lookup_input_is_256_then_multiplicity_is_0, + ] + .map(|circuit| circuit.consume()) + .to_vec() } pub fn ext_transition_constraints_as_circuits() -> Vec> { - vec![] + let circuit_builder = ConstraintCircuitBuilder::new(); + let one = circuit_builder.b_constant(BFIELD_ONE); + let twofiftysix = circuit_builder.b_constant(BFieldElement::new(1 << 8)); + + let current_base_row = |col_id: LookupBaseTableColumn| { + circuit_builder.input(CurrentBaseRow(col_id.master_base_table_index())) + }; + let next_base_row = |col_id: LookupBaseTableColumn| { + circuit_builder.input(NextBaseRow(col_id.master_base_table_index())) + }; + let current_ext_row = |col_id: LookupExtTableColumn| { + circuit_builder.input(CurrentExtRow(col_id.master_ext_table_index())) + }; + let next_ext_row = |col_id: LookupExtTableColumn| { + circuit_builder.input(NextExtRow(col_id.master_ext_table_index())) + }; + let challenge = |challenge_id: ChallengeId| circuit_builder.challenge(challenge_id); + + let lookup_input = current_base_row(LookIn); + let inverse_of_2_pow_8_minus_lookup_input = current_base_row(InverseOf2Pow8MinusLookIn); + let cascade_table_server_log_derivative = current_ext_row(CascadeTableServerLogDerivative); + let public_evaluation_argument = current_ext_row(PublicEvaluationArgument); + + let lookup_input_next = next_base_row(LookIn); + let lookup_output_next = next_base_row(LookOut); + let lookup_multiplicity_next = next_base_row(LookupMultiplicity); + let inverse_of_2_pow_8_minus_lookup_input_next = next_base_row(InverseOf2Pow8MinusLookIn); + let cascade_table_server_log_derivative_next = + next_ext_row(CascadeTableServerLogDerivative); + let public_evaluation_argument_next = next_ext_row(PublicEvaluationArgument); + + // Lookup Table's input increments by 1 if and only if it is less than 256. + let lookup_input_is_unequal_to_256 = (twofiftysix.clone() - lookup_input.clone()) + * inverse_of_2_pow_8_minus_lookup_input + - one.clone(); + let if_lookup_input_is_equal_to_256_then_lookup_input_next_is_256 = + lookup_input_is_unequal_to_256 * (lookup_input_next.clone() - twofiftysix.clone()); + let if_lookup_input_is_unequal_to_256_then_lookup_input_next_increments_by_1 = + (lookup_input.clone() - twofiftysix.clone()) + * (lookup_input_next.clone() - lookup_input - one.clone()); + let lookup_input_increments_if_and_only_if_less_than_256 = + if_lookup_input_is_equal_to_256_then_lookup_input_next_is_256 + + if_lookup_input_is_unequal_to_256_then_lookup_input_next_increments_by_1; + + // helper variables to determine whether the next row is a padding row + let lookup_input_next_is_unequal_to_256 = (twofiftysix.clone() - lookup_input_next.clone()) + * inverse_of_2_pow_8_minus_lookup_input_next + - one; + let lookup_input_next_is_equal_to_256 = twofiftysix - lookup_input_next.clone(); + + // Lookup Argument with Cascade Table + let cascade_table_indeterminate = challenge(CascadeLookupIndeterminate); + let compressed_row = lookup_input_next * challenge(LookupTableInputWeight) + + lookup_output_next.clone() * challenge(LookupTableOutputWeight); + let cascade_table_log_derivative_remains = cascade_table_server_log_derivative_next.clone() + - cascade_table_server_log_derivative.clone(); + let cascade_table_log_derivative_updates = (cascade_table_server_log_derivative_next + - cascade_table_server_log_derivative) + * (cascade_table_indeterminate - compressed_row) + - lookup_multiplicity_next; + let cascade_table_log_derivative_updates_if_and_only_if_next_row_is_not_padding_row = + lookup_input_next_is_equal_to_256.clone() * cascade_table_log_derivative_updates + + lookup_input_next_is_unequal_to_256.clone() + * cascade_table_log_derivative_remains; + + // public Evaluation Argument + let public_indeterminate = challenge(LookupTablePublicIndeterminate); + let public_evaluation_argument_remains = + public_evaluation_argument_next.clone() - public_evaluation_argument.clone(); + let public_evaluation_argument_updates = public_evaluation_argument_next + - public_evaluation_argument * public_indeterminate + - lookup_output_next; + let public_evaluation_argument_updates_if_and_only_if_next_row_is_not_padding_row = + lookup_input_next_is_equal_to_256 * public_evaluation_argument_updates + + lookup_input_next_is_unequal_to_256 * public_evaluation_argument_remains; + + [ + lookup_input_increments_if_and_only_if_less_than_256, + cascade_table_log_derivative_updates_if_and_only_if_next_row_is_not_padding_row, + public_evaluation_argument_updates_if_and_only_if_next_row_is_not_padding_row, + ] + .map(|circuit| circuit.consume()) + .to_vec() } pub fn ext_terminal_constraints_as_circuits() -> Vec> { - vec![] + let circuit_builder = ConstraintCircuitBuilder::new(); + let twofiftyfive = circuit_builder.b_constant(BFieldElement::new(255)); + let twofiftysix = circuit_builder.b_constant(BFieldElement::new(256)); + + let lookup_input = circuit_builder.input(BaseRow(LookIn.master_base_table_index())); + + let lookup_input_is_255_or_256 = + (lookup_input.clone() - twofiftyfive) * (lookup_input - twofiftysix); + + [lookup_input_is_255_or_256] + .map(|circuit| circuit.consume()) + .to_vec() } } diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index d9f0af152..2a91ed870 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -314,15 +314,17 @@ pub enum CascadeExtTableColumn { #[repr(usize)] #[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] pub enum LookupBaseTableColumn { - /// Indicates whether the current row is a padding row. - IsPadding, - /// The lookup input. LookIn, /// The lookup output. LookOut, + /// The inverse-or-zero of 2^8 minus the lookup input. Helps + /// - identifying padding rows, and + /// - enforcing that the padding section is started in the correct row. + InverseOf2Pow8MinusLookIn, + /// The number of times the value is looked up. LookupMultiplicity, } From 2fee39427818768069af4458d9a9e888ec4ff95d Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 1 Mar 2023 18:48:04 +0100 Subject: [PATCH 17/49] update AET relations diagram --- specification/src/img/aet-relations.ipe | 28 +++++++++++++++++++++--- specification/src/img/aet-relations.pdf | Bin 27267 -> 28474 bytes specification/src/img/aet-relations.png | Bin 28748 -> 32865 bytes 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/specification/src/img/aet-relations.ipe b/specification/src/img/aet-relations.ipe index 71d85c73c..2ab812c62 100644 --- a/specification/src/img/aet-relations.ipe +++ b/specification/src/img/aet-relations.ipe @@ -1,7 +1,7 @@ - + \usepackage{lmodern} \renewcommand*\familydefault{\sfdefault} \usepackage[T1]{fontenc} @@ -322,8 +322,8 @@ h -120 172 m -120 60 l +72 172 m +72 60 l 272 60 l 272 172 l h @@ -437,5 +437,27 @@ h 194.5 92 m 194.5 124 l +cascade +lookup + +117.179 120 m +122.97 120 l + + +117.179 120 m +122.97 120 l + + +128 120 m +112 120 l + + +128.936 116 m +119.212 116 l + + +192 138 m +192 154 l + diff --git a/specification/src/img/aet-relations.pdf b/specification/src/img/aet-relations.pdf index 56ad5849f7058cc7bfb9d181309c192b79bb41c8..b974fabe2fbe7f5b534a710b7742dc0a1dfb9a4d 100644 GIT binary patch delta 21133 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 U@b|R=LsM%KC@Fc?@Eq{}04+-ZSO5S3 delta 20557 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 B1^)m5 literal 28748 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 MeP!7~jG_Pk0oUNKjsO4v From fd12bc5ce43cf91d9ba26e2dd3d5d7ddb59fc185 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 3 Mar 2023 23:13:45 +0100 Subject: [PATCH 18/49] remove superfluous cloning and imports --- triton-vm/src/table/hash_table.rs | 35 ++++++++++++++----------------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index ce4432967..c8e919637 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -1,6 +1,5 @@ use itertools::Itertools; use ndarray::s; -use ndarray::Array1; use ndarray::ArrayView1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; @@ -89,9 +88,9 @@ impl ExtHashTable { + base_row(State2MidHighLkIn) * two_pow_32.clone() + base_row(State2MidLowLkIn) * two_pow_16.clone() + base_row(State2LowestLkIn); - let state_3 = base_row(State3HighestLkIn) * two_pow_48.clone() - + base_row(State3MidHighLkIn) * two_pow_32.clone() - + base_row(State3MidLowLkIn) * two_pow_16.clone() + let state_3 = base_row(State3HighestLkIn) * two_pow_48 + + base_row(State3MidHighLkIn) * two_pow_32 + + base_row(State3MidLowLkIn) * two_pow_16 + base_row(State3LowestLkIn); let state = [ @@ -143,8 +142,7 @@ impl ExtHashTable { let running_evaluation_hash_input_has_accumulated_first_row = running_evaluation_hash_input - running_evaluation_initial.clone() * hash_input_indeterminate - compressed_row.clone(); - let running_evaluation_hash_input_is_initialized_correctly = (round_number.clone() - + one.clone()) + let running_evaluation_hash_input_is_initialized_correctly = (round_number.clone() + one) * ci_is_absorb_init.clone() * running_evaluation_hash_input_has_accumulated_first_row + ci_is_hash.clone() * running_evaluation_hash_input_is_default_initial.clone() @@ -238,12 +236,10 @@ impl ExtHashTable { let if_ci_is_hash_and_round_no_is_0_then_state_14_is_1 = if_ci_is_hash_and_round_no_is_0_then_.clone() * (state14.clone() - constant(1)); let if_ci_is_hash_and_round_no_is_0_then_state_15_is_1 = - if_ci_is_hash_and_round_no_is_0_then_.clone() * (state15.clone() - constant(1)); + if_ci_is_hash_and_round_no_is_0_then_ * (state15.clone() - constant(1)); - let if_ci_is_absorb_init_and_round_no_is_0_then_ = round_number_is_not_0.clone() - * ci_is_hash - * ci_is_absorb.clone() - * ci_is_squeeze.clone(); + let if_ci_is_absorb_init_and_round_no_is_0_then_ = + round_number_is_not_0 * ci_is_hash * ci_is_absorb * ci_is_squeeze; let if_ci_is_absorb_init_and_round_no_is_0_then_state_10_is_0 = if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state10; let if_ci_is_absorb_init_and_round_no_is_0_then_state_11_is_0 = @@ -255,7 +251,7 @@ impl ExtHashTable { let if_ci_is_absorb_init_and_round_no_is_0_then_state_14_is_0 = if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state14; let if_ci_is_absorb_init_and_round_no_is_0_then_state_15_is_0 = - if_ci_is_absorb_init_and_round_no_is_0_then_.clone() * state15; + if_ci_is_absorb_init_and_round_no_is_0_then_ * state15; let mut constraints = vec![ if_padding_row_then_ci_is_hash, @@ -379,9 +375,9 @@ impl ExtHashTable { + current_base_row(State2MidHighLkIn) * two_pow_32.clone() + current_base_row(State2MidLowLkIn) * two_pow_16.clone() + current_base_row(State2LowestLkIn); - let state_3 = current_base_row(State3HighestLkIn) * two_pow_48.clone() - + current_base_row(State3MidHighLkIn) * two_pow_32.clone() - + current_base_row(State3MidLowLkIn) * two_pow_16.clone() + let state_3 = current_base_row(State3HighestLkIn) * two_pow_48 + + current_base_row(State3MidHighLkIn) * two_pow_32 + + current_base_row(State3MidLowLkIn) * two_pow_16 + current_base_row(State3LowestLkIn); let state_current = [ @@ -454,7 +450,7 @@ impl ExtHashTable { * (ci_next.clone() - opcode_hash.clone()); let if_round_number_is_not_5_then_ci_doesnt_change = - (round_number.clone() - constant(NUM_ROUNDS as u64)) * (ci_next.clone() - ci); + (round_number - constant(NUM_ROUNDS as u64)) * (ci_next.clone() - ci); // copy capacity between rounds with index 5 and 0 if instruction is “absorb” let round_number_next_is_not_0 = @@ -554,12 +550,12 @@ impl ExtHashTable { .sum(); let running_evaluation_sponge_has_accumulated_next_row = running_evaluation_sponge_next .clone() - - sponge_indeterminate.clone() * running_evaluation_sponge.clone() + - sponge_indeterminate * running_evaluation_sponge.clone() - challenge(HashCIWeight) * ci_next.clone() - compressed_row_next; let if_round_no_next_0_and_ci_next_is_spongy_then_running_eval_sponge_updates = - round_number_next_is_not_0.clone() - * (ci_next.clone() - opcode_hash.clone()) + round_number_next_is_not_0 + * (ci_next.clone() - opcode_hash) * running_evaluation_sponge_has_accumulated_next_row; let running_evaluation_sponge_absorb_remains = @@ -673,6 +669,7 @@ impl ExtHashTable { state_part_after_power_map[11].clone(), ]; + #[allow(clippy::needless_range_loop)] let state_after_matrix_multiplication: [_; STATE_SIZE] = { let mut result_vec = Vec::with_capacity(STATE_SIZE); for row_idx in 0..STATE_SIZE { From 61aa4924c25046d380c91325e5f8f0ed2176c081 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 3 Mar 2023 23:18:54 +0100 Subject: [PATCH 19/49] add debug identifier information for new table's quotients --- triton-vm/src/table/master_table.rs | 34 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/triton-vm/src/table/master_table.rs b/triton-vm/src/table/master_table.rs index a48626d66..361de4c88 100644 --- a/triton-vm/src/table/master_table.rs +++ b/triton-vm/src/table/master_table.rs @@ -1613,7 +1613,7 @@ pub fn constraint_type_and_index_and_table_name( let transition_section_end = transition_section_start + num_all_transition_quotients(); let terminal_section_start = transition_section_end; let terminal_section_end = terminal_section_start + num_all_terminal_quotients(); - + assert_eq!(num_all_table_quotients(), terminal_section_end); match constraint_idx { idx if initial_section_start <= idx && idx < initial_section_end => { let section_idx = idx - initial_section_start; @@ -1656,7 +1656,11 @@ pub fn initial_constraint_table_idx_and_name(constraint_idx: usize) -> (usize, & let jump_stack_end = jump_stack_start + ExtJumpStackTable::num_initial_quotients(); let hash_start = jump_stack_end; let hash_end = hash_start + ExtHashTable::num_initial_quotients(); - let u32_start = hash_end; + let cascade_start = hash_end; + let cascade_end = cascade_start + ExtCascadeTable::num_initial_quotients(); + let lookup_start = cascade_end; + let lookup_end = lookup_start + ExtLookupTable::num_initial_quotients(); + let u32_start = lookup_end; let u32_end = u32_start + ExtU32Table::num_initial_quotients(); assert_eq!(num_all_initial_quotients(), u32_end); match constraint_idx { @@ -1666,6 +1670,8 @@ pub fn initial_constraint_table_idx_and_name(constraint_idx: usize) -> (usize, & i if ram_start <= i && i < ram_end => (i - ram_start, "Ram"), i if jump_stack_start <= i && i < jump_stack_end => (i - jump_stack_start, "JumpStack"), i if hash_start <= i && i < hash_end => (i - hash_start, "Hash"), + i if cascade_start <= i && i < cascade_end => (i - cascade_start, "Cascade"), + i if lookup_start <= i && i < lookup_end => (i - lookup_start, "Lookup"), i if u32_start <= i && i < u32_end => (i - u32_start, "U32"), _ => (0, "Unknown"), } @@ -1688,7 +1694,11 @@ pub fn consistency_constraint_table_idx_and_name(constraint_idx: usize) -> (usiz let jump_stack_end = jump_stack_start + ExtJumpStackTable::num_consistency_quotients(); let hash_start = jump_stack_end; let hash_end = hash_start + ExtHashTable::num_consistency_quotients(); - let u32_start = hash_end; + let cascade_start = hash_end; + let cascade_end = cascade_start + ExtCascadeTable::num_consistency_quotients(); + let lookup_start = cascade_end; + let lookup_end = lookup_start + ExtLookupTable::num_consistency_quotients(); + let u32_start = lookup_end; let u32_end = u32_start + ExtU32Table::num_consistency_quotients(); assert_eq!(num_all_consistency_quotients(), u32_end); match constraint_idx { @@ -1698,6 +1708,8 @@ pub fn consistency_constraint_table_idx_and_name(constraint_idx: usize) -> (usiz i if ram_start <= i && i < ram_end => (i - ram_start, "Ram"), i if jump_stack_start <= i && i < jump_stack_end => (i - jump_stack_start, "JumpStack"), i if hash_start <= i && i < hash_end => (i - hash_start, "Hash"), + i if cascade_start <= i && i < cascade_end => (i - cascade_start, "Cascade"), + i if lookup_start <= i && i < lookup_end => (i - lookup_start, "Lookup"), i if u32_start <= i && i < u32_end => (i - u32_start, "U32"), _ => (0, "Unknown"), } @@ -1720,7 +1732,11 @@ pub fn transition_constraint_table_idx_and_name(constraint_idx: usize) -> (usize let jump_stack_end = jump_stack_start + ExtJumpStackTable::num_transition_quotients(); let hash_start = jump_stack_end; let hash_end = hash_start + ExtHashTable::num_transition_quotients(); - let u32_start = hash_end; + let cascade_start = hash_end; + let cascade_end = cascade_start + ExtCascadeTable::num_transition_quotients(); + let lookup_start = cascade_end; + let lookup_end = lookup_start + ExtLookupTable::num_transition_quotients(); + let u32_start = lookup_end; let u32_end = u32_start + ExtU32Table::num_transition_quotients(); assert_eq!(num_all_transition_quotients(), u32_end); match constraint_idx { @@ -1730,6 +1746,8 @@ pub fn transition_constraint_table_idx_and_name(constraint_idx: usize) -> (usize i if ram_start <= i && i < ram_end => (i - ram_start, "Ram"), i if jump_stack_start <= i && i < jump_stack_end => (i - jump_stack_start, "JumpStack"), i if hash_start <= i && i < hash_end => (i - hash_start, "Hash"), + i if cascade_start <= i && i < cascade_end => (i - cascade_start, "Cascade"), + i if lookup_start <= i && i < lookup_end => (i - lookup_start, "Lookup"), i if u32_start <= i && i < u32_end => (i - u32_start, "U32"), _ => (0, "Unknown"), } @@ -1752,7 +1770,11 @@ pub fn terminal_constraint_table_idx_and_name(constraint_idx: usize) -> (usize, let jump_stack_end = jump_stack_start + ExtJumpStackTable::num_terminal_quotients(); let hash_start = jump_stack_end; let hash_end = hash_start + ExtHashTable::num_terminal_quotients(); - let u32_start = hash_end; + let cascade_start = hash_end; + let cascade_end = cascade_start + ExtCascadeTable::num_terminal_quotients(); + let lookup_start = cascade_end; + let lookup_end = lookup_start + ExtLookupTable::num_terminal_quotients(); + let u32_start = lookup_end; let u32_end = u32_start + ExtU32Table::num_terminal_quotients(); let cross_table_start = u32_end; let cross_table_end = cross_table_start + GrandCrossTableArg::num_terminal_quotients(); @@ -1764,6 +1786,8 @@ pub fn terminal_constraint_table_idx_and_name(constraint_idx: usize) -> (usize, i if ram_start <= i && i < ram_end => (i - ram_start, "Ram"), i if jump_stack_start <= i && i < jump_stack_end => (i - jump_stack_start, "JumpStack"), i if hash_start <= i && i < hash_end => (i - hash_start, "Hash"), + i if cascade_start <= i && i < cascade_end => (i - cascade_start, "Cascade"), + i if lookup_start <= i && i < lookup_end => (i - lookup_start, "Lookup"), i if u32_start <= i && i < u32_end => (i - u32_start, "U32"), i if cross_table_start <= i && i < cross_table_end => { (i - cross_table_start, "GrandCrossTableArgument") From 130ecd4bcafb5379a5eb318217e9457de7459718 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 3 Mar 2023 23:26:19 +0100 Subject: [PATCH 20/49] fix usize underflow --- triton-vm/src/table/hash_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index c8e919637..ee012673f 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -764,7 +764,7 @@ impl HashTable { pub const fn mds_matrix_entry(row_idx: usize, col_idx: usize) -> BFieldElement { assert!(row_idx < STATE_SIZE); assert!(col_idx < STATE_SIZE); - let index_in_matrix_defining_column = (row_idx - col_idx) % STATE_SIZE; + let index_in_matrix_defining_column = (STATE_SIZE + row_idx - col_idx) % STATE_SIZE; let mds_matrix_entry = MDS_MATRIX_FIRST_COLUMN[index_in_matrix_defining_column]; BFieldElement::new(mds_matrix_entry as u64) } From 49aea7bd28dee98c036607d0eb1a7691492f6b24 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Sat, 4 Mar 2023 09:19:52 +0100 Subject: [PATCH 21/49] fold constants in new Lookup & Cascade Tables --- triton-vm/src/table/cascade_table.rs | 19 +++++++++----- triton-vm/src/table/lookup_table.rs | 37 ++++++++++++++-------------- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index c21cbd19b..eeebaffa2 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -1,14 +1,13 @@ -use ndarray::s; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; -use twenty_first::shared_math::b_field_element::BFIELD_ONE; use twenty_first::shared_math::tip5; use twenty_first::shared_math::x_field_element::XFieldElement; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; +use crate::table::constraint_circuit::ConstraintCircuitMonad; use crate::table::constraint_circuit::DualRowIndicator; use crate::table::constraint_circuit::SingleRowIndicator; use crate::table::table_column::CascadeBaseTableColumn; @@ -58,18 +57,26 @@ impl CascadeTable { impl ExtCascadeTable { pub fn ext_initial_constraints_as_circuits() -> Vec> { - vec![] + let mut constraints = []; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_consistency_constraints_as_circuits() -> Vec> { - vec![] + let mut constraints = []; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_transition_constraints_as_circuits() -> Vec> { - vec![] + let mut constraints = []; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_terminal_constraints_as_circuits() -> Vec> { - vec![] + let mut constraints = []; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } } diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index f2fea97fc..e275d88bd 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -18,6 +18,7 @@ use crate::table::challenges::ChallengeId::LookupTablePublicIndeterminate; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; use crate::table::constraint_circuit::ConstraintCircuitBuilder; +use crate::table::constraint_circuit::ConstraintCircuitMonad; use crate::table::constraint_circuit::DualRowIndicator; use crate::table::constraint_circuit::DualRowIndicator::*; use crate::table::constraint_circuit::SingleRowIndicator; @@ -154,14 +155,14 @@ impl ExtLookupTable { let cascade_table_server_log_derivative = ext_row(CascadeTableServerLogDerivative); let public_evaluation_argument = ext_row(PublicEvaluationArgument); - let lookup_input_is_0 = lookup_input.clone(); + let lookup_input_is_0 = lookup_input; // Lookup Argument with Cascade Table + // note: `lookup_input` is known to be 0 and thus doesn't appear in the compressed row let lookup_argument_default_initial = circuit_builder.x_constant(LookupArg::default_initial()); let cascade_table_indeterminate = challenge(CascadeLookupIndeterminate); - let compressed_row = lookup_input * challenge(LookupTableInputWeight) - + lookup_output.clone() * challenge(LookupTableOutputWeight); + let compressed_row = lookup_output.clone() * challenge(LookupTableOutputWeight); let cascade_table_log_derivative_is_initialized_correctly = (cascade_table_server_log_derivative - lookup_argument_default_initial) * (cascade_table_indeterminate - compressed_row) @@ -174,13 +175,13 @@ impl ExtLookupTable { - eval_argument_default_initial * public_indeterminate - lookup_output; - [ + let mut constraints = [ lookup_input_is_0, cascade_table_log_derivative_is_initialized_correctly, public_evaluation_argument_is_initialized_correctly, - ] - .map(|circuit| circuit.consume()) - .to_vec() + ]; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_consistency_constraints_as_circuits() -> Vec> { @@ -209,13 +210,13 @@ impl ExtLookupTable { let if_lookup_input_is_256_then_multiplicity_is_0 = two_pow_8_minus_lookup_input_times_inv_is_1 * lookup_multiplicity; - [ + let mut constraints = [ two_pow_8_minus_lookup_input_times_inv_is_1_or_lookup_input_is_two_pow_8, two_pow_8_minus_lookup_input_times_inv_is_1_or_inv_is_zero, if_lookup_input_is_256_then_multiplicity_is_0, - ] - .map(|circuit| circuit.consume()) - .to_vec() + ]; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_transition_constraints_as_circuits() -> Vec> { @@ -295,13 +296,13 @@ impl ExtLookupTable { lookup_input_next_is_equal_to_256 * public_evaluation_argument_updates + lookup_input_next_is_unequal_to_256 * public_evaluation_argument_remains; - [ + let mut constraints = [ lookup_input_increments_if_and_only_if_less_than_256, cascade_table_log_derivative_updates_if_and_only_if_next_row_is_not_padding_row, public_evaluation_argument_updates_if_and_only_if_next_row_is_not_padding_row, - ] - .map(|circuit| circuit.consume()) - .to_vec() + ]; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_terminal_constraints_as_circuits() -> Vec> { @@ -314,8 +315,8 @@ impl ExtLookupTable { let lookup_input_is_255_or_256 = (lookup_input.clone() - twofiftyfive) * (lookup_input - twofiftysix); - [lookup_input_is_255_or_256] - .map(|circuit| circuit.consume()) - .to_vec() + let mut constraints = [lookup_input_is_255_or_256]; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } } From 4ad8d1806533e9f92f75b5f26ac275c309d263d1 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Sat, 4 Mar 2023 09:39:22 +0100 Subject: [PATCH 22/49] correctly identify padding rows when extending Lookup Table --- triton-vm/src/table/lookup_table.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index e275d88bd..eea97232d 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -2,10 +2,10 @@ use ndarray::s; use ndarray::Array1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; +use num_traits::Zero; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; -use twenty_first::shared_math::b_field_element::BFIELD_ZERO; use twenty_first::shared_math::tip5; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; @@ -117,9 +117,9 @@ impl LookupTable { let lookup_input = base_row[LookIn.base_table_index()]; let lookup_output = base_row[LookOut.base_table_index()]; let lookup_multiplicity = base_row[LookupMultiplicity.base_table_index()]; - let is_padding = base_row[InverseOf2Pow8MinusLookIn.base_table_index()] == BFIELD_ZERO; + let is_padding = base_row[InverseOf2Pow8MinusLookIn.base_table_index()].is_zero(); - if is_padding { + if !is_padding { let compressed_row = lookup_input * look_in_weight + lookup_output * look_out_weight; cascade_table_running_sum_log_derivative += From 41f235e7ac24913c7fd8989fbe419bee28e58dd2 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Sat, 4 Mar 2023 10:24:22 +0100 Subject: [PATCH 23/49] improve code readability when extending Hash Table --- triton-vm/src/table/hash_table.rs | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index ee012673f..1368bdd02 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -884,22 +884,20 @@ impl HashTable { .map(|(&weight, &element)| weight * element) .sum(); - match current_instruction { - ci if ci == opcode_hash => { - hash_input_running_evaluation = hash_input_running_evaluation - * hash_input_eval_indeterminate - + compressed_row; - } - ci if ci == opcode_absorb_init - || ci == opcode_absorb - || ci == opcode_squeeze => - { - sponge_running_evaluation = sponge_running_evaluation - * sponge_eval_indeterminate - + ci_weight * ci - + compressed_row; - } - _ => panic!("Opcode must be of `hash`, `absorb_init`, `absorb`, or `squeeze`."), + if current_instruction == opcode_hash { + hash_input_running_evaluation = hash_input_running_evaluation + * hash_input_eval_indeterminate + + compressed_row; + } + + if current_instruction == opcode_absorb_init + || current_instruction == opcode_absorb + || current_instruction == opcode_squeeze + { + sponge_running_evaluation = sponge_running_evaluation + * sponge_eval_indeterminate + + ci_weight * current_instruction + + compressed_row; } } From 813403bb871042367f7a54d6780eafcd40fceb6c Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Sat, 4 Mar 2023 10:25:24 +0100 Subject: [PATCH 24/49] fix bug in round number deselector in Hash Table --- triton-vm/src/table/hash_table.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 1368bdd02..c9dd5ed41 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -186,11 +186,16 @@ impl ExtHashTable { "Round number to deselect must be in the range [-1, {NUM_ROUNDS}] \ but got {round_number_to_deselect}." ); + // Because BFieldElements cannot be built from signed integers, we special-case -1. let constant = |c: u64| circuit_builder.b_constant(c.into()); - (-1..=NUM_ROUNDS as isize) + let factor_for_neg_1 = match round_number_to_deselect == -1 { + true => constant(1), + false => round_number_circuit_node.clone() + constant(1), + }; + (0..=NUM_ROUNDS as isize) .filter(|&r| r != round_number_to_deselect) .map(|r| round_number_circuit_node.clone() - constant(r as u64)) - .fold(constant(1), |a, b| a * b) + .fold(factor_for_neg_1, |a, b| a * b) } pub fn ext_consistency_constraints_as_circuits() -> Vec> { From 4ba59dee487c4bb6bf99c7f35e69150f5aacde50 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Sat, 4 Mar 2023 11:12:21 +0100 Subject: [PATCH 25/49] correctly index BFieldElement's 16-bit limbs based on documented order --- triton-vm/src/vm.rs | 68 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index b4ea72666..4a41b31bc 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -1046,58 +1046,58 @@ impl AlgebraicExecutionTrace { let st_0_raw_limbs = trace_row[0].raw_u16s(); let st_0_look_in_split = st_0_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State0HighestLkIn.base_table_index()] = st_0_look_in_split[0]; - row[State0MidHighLkIn.base_table_index()] = st_0_look_in_split[1]; - row[State0MidLowLkIn.base_table_index()] = st_0_look_in_split[2]; - row[State0LowestLkIn.base_table_index()] = st_0_look_in_split[3]; + row[State0LowestLkIn.base_table_index()] = st_0_look_in_split[0]; + row[State0MidLowLkIn.base_table_index()] = st_0_look_in_split[1]; + row[State0MidHighLkIn.base_table_index()] = st_0_look_in_split[2]; + row[State0HighestLkIn.base_table_index()] = st_0_look_in_split[3]; let st_0_look_out_split = st_0_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State0HighestLkOut.base_table_index()] = st_0_look_out_split[0]; - row[State0MidHighLkOut.base_table_index()] = st_0_look_out_split[1]; - row[State0MidLowLkOut.base_table_index()] = st_0_look_out_split[2]; - row[State0LowestLkOut.base_table_index()] = st_0_look_out_split[3]; + row[State0LowestLkOut.base_table_index()] = st_0_look_out_split[0]; + row[State0MidLowLkOut.base_table_index()] = st_0_look_out_split[1]; + row[State0MidHighLkOut.base_table_index()] = st_0_look_out_split[2]; + row[State0HighestLkOut.base_table_index()] = st_0_look_out_split[3]; let st_1_raw_limbs = trace_row[1].raw_u16s(); let st_1_look_in_split = st_1_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State1HighestLkIn.base_table_index()] = st_1_look_in_split[0]; - row[State1MidHighLkIn.base_table_index()] = st_1_look_in_split[1]; - row[State1MidLowLkIn.base_table_index()] = st_1_look_in_split[2]; - row[State1LowestLkIn.base_table_index()] = st_1_look_in_split[3]; + row[State1LowestLkIn.base_table_index()] = st_1_look_in_split[0]; + row[State1MidLowLkIn.base_table_index()] = st_1_look_in_split[1]; + row[State1MidHighLkIn.base_table_index()] = st_1_look_in_split[2]; + row[State1HighestLkIn.base_table_index()] = st_1_look_in_split[3]; let st_1_look_out_split = st_1_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State1HighestLkOut.base_table_index()] = st_1_look_out_split[0]; - row[State1MidHighLkOut.base_table_index()] = st_1_look_out_split[1]; - row[State1MidLowLkOut.base_table_index()] = st_1_look_out_split[2]; - row[State1LowestLkOut.base_table_index()] = st_1_look_out_split[3]; + row[State1LowestLkOut.base_table_index()] = st_1_look_out_split[0]; + row[State1MidLowLkOut.base_table_index()] = st_1_look_out_split[1]; + row[State1MidHighLkOut.base_table_index()] = st_1_look_out_split[2]; + row[State1HighestLkOut.base_table_index()] = st_1_look_out_split[3]; let st_2_raw_limbs = trace_row[2].raw_u16s(); let st_2_look_in_split = st_2_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State2HighestLkIn.base_table_index()] = st_2_look_in_split[0]; - row[State2MidHighLkIn.base_table_index()] = st_2_look_in_split[1]; - row[State2MidLowLkIn.base_table_index()] = st_2_look_in_split[2]; - row[State2LowestLkIn.base_table_index()] = st_2_look_in_split[3]; + row[State2LowestLkIn.base_table_index()] = st_2_look_in_split[0]; + row[State2MidLowLkIn.base_table_index()] = st_2_look_in_split[1]; + row[State2MidHighLkIn.base_table_index()] = st_2_look_in_split[2]; + row[State2HighestLkIn.base_table_index()] = st_2_look_in_split[3]; let st_2_look_out_split = st_2_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State2HighestLkOut.base_table_index()] = st_2_look_out_split[0]; - row[State2MidHighLkOut.base_table_index()] = st_2_look_out_split[1]; - row[State2MidLowLkOut.base_table_index()] = st_2_look_out_split[2]; - row[State2LowestLkOut.base_table_index()] = st_2_look_out_split[3]; + row[State2LowestLkOut.base_table_index()] = st_2_look_out_split[0]; + row[State2MidLowLkOut.base_table_index()] = st_2_look_out_split[1]; + row[State2MidHighLkOut.base_table_index()] = st_2_look_out_split[2]; + row[State2HighestLkOut.base_table_index()] = st_2_look_out_split[3]; let st_3_raw_limbs = trace_row[3].raw_u16s(); let st_3_look_in_split = st_3_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State3HighestLkIn.base_table_index()] = st_3_look_in_split[0]; - row[State3MidHighLkIn.base_table_index()] = st_3_look_in_split[1]; - row[State3MidLowLkIn.base_table_index()] = st_3_look_in_split[2]; - row[State3LowestLkIn.base_table_index()] = st_3_look_in_split[3]; + row[State3LowestLkIn.base_table_index()] = st_3_look_in_split[0]; + row[State3MidLowLkIn.base_table_index()] = st_3_look_in_split[1]; + row[State3MidHighLkIn.base_table_index()] = st_3_look_in_split[2]; + row[State3HighestLkIn.base_table_index()] = st_3_look_in_split[3]; let st_3_look_out_split = st_3_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State3HighestLkOut.base_table_index()] = st_3_look_out_split[0]; - row[State3MidHighLkOut.base_table_index()] = st_3_look_out_split[1]; - row[State3MidLowLkOut.base_table_index()] = st_3_look_out_split[2]; - row[State3LowestLkOut.base_table_index()] = st_3_look_out_split[3]; + row[State3LowestLkOut.base_table_index()] = st_3_look_out_split[0]; + row[State3MidLowLkOut.base_table_index()] = st_3_look_out_split[1]; + row[State3MidHighLkOut.base_table_index()] = st_3_look_out_split[2]; + row[State3HighestLkOut.base_table_index()] = st_3_look_out_split[3]; row[State4.base_table_index()] = trace_row[4]; row[State5.base_table_index()] = trace_row[5]; @@ -1161,8 +1161,8 @@ impl AlgebraicExecutionTrace { /// significant limb. fn inverse_or_zero_of_highest_2_limbs(state_element: BFieldElement) -> BFieldElement { let limbs = state_element.raw_u16s().map(|limb| limb as u64); - let highest = limbs[0]; - let mid_high = limbs[1]; + let highest = limbs[3]; + let mid_high = limbs[2]; let to_invert = mid_high + (highest << 16) - (1 << 32) + 1; BFieldElement::from_raw_u64(to_invert).inverse_or_zero() } From d8d0fa4f2c4087ec793d6a4b810c0f591ab0c0e6 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Sun, 5 Mar 2023 21:33:07 +0100 Subject: [PATCH 26/49] correctly absorb elements into Tip5's Sponge: replace, don't add --- triton-vm/src/vm.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 4a41b31bc..4086cb878 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -337,12 +337,7 @@ impl<'pgm> VMState<'pgm> { if self.current_instruction()? == AbsorbInit { self.sponge_state = Tip5State::new(Domain::VariableLength).state; } - self.sponge_state[..tip5::RATE] - .iter_mut() - .zip_eq(to_absorb.iter()) - .for_each(|(sponge_state_element, &to_absorb_element)| { - *sponge_state_element += to_absorb_element; - }); + self.sponge_state[..tip5::RATE].copy_from_slice(&to_absorb); let tip5_trace = Tip5::trace(&mut Tip5State { state: self.sponge_state, }); From 3b00ee6f43d32230b7a7a529204b73d02356394f Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 6 Mar 2023 15:45:44 +0100 Subject: [PATCH 27/49] evaluate terminal constraints in order consistent with other constraints --- triton-vm/src/table/master_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triton-vm/src/table/master_table.rs b/triton-vm/src/table/master_table.rs index 361de4c88..12825afeb 100644 --- a/triton-vm/src/table/master_table.rs +++ b/triton-vm/src/table/master_table.rs @@ -1551,9 +1551,9 @@ pub fn evaluate_all_terminal_constraints( ExtRamTable::evaluate_terminal_constraints(base_row, ext_row, challenges), ExtJumpStackTable::evaluate_terminal_constraints(base_row, ext_row, challenges), ExtHashTable::evaluate_terminal_constraints(base_row, ext_row, challenges), - ExtU32Table::evaluate_terminal_constraints(base_row, ext_row, challenges), ExtCascadeTable::evaluate_terminal_constraints(base_row, ext_row, challenges), ExtLookupTable::evaluate_terminal_constraints(base_row, ext_row, challenges), + ExtU32Table::evaluate_terminal_constraints(base_row, ext_row, challenges), GrandCrossTableArg::evaluate_terminal_constraints(base_row, ext_row, challenges), ] .concat() From 3d3f0db494c282f368b870abc11ea16fdfdef365 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 6 Mar 2023 18:17:17 +0100 Subject: [PATCH 28/49] add consistency constraints for correct limb decomposition to Hash Table Also: - fix bug: correctly compute inverses - fix bug: correctly set inverses when padding Hash Table - move construction of Hash Table rows from `vm.rs` to `hash_table.rs` --- triton-vm/src/table/hash_table.rs | 250 +++++++++++++++++++++++++++++- triton-vm/src/vm.rs | 153 +----------------- 2 files changed, 253 insertions(+), 150 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index c9dd5ed41..43e7738bd 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -1,13 +1,14 @@ use itertools::Itertools; use ndarray::s; +use ndarray::Array2; use ndarray::ArrayView1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; use num_traits::Zero; use strum::EnumCount; -use triton_opcodes::instruction::Instruction; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; +use twenty_first::shared_math::b_field_element::BFIELD_ZERO; use twenty_first::shared_math::tip5::DIGEST_LENGTH; use twenty_first::shared_math::tip5::MDS_MATRIX_FIRST_COLUMN; use twenty_first::shared_math::tip5::NUM_ROUNDS; @@ -15,8 +16,12 @@ use twenty_first::shared_math::tip5::NUM_SPLIT_AND_LOOKUP; use twenty_first::shared_math::tip5::RATE; use twenty_first::shared_math::tip5::ROUND_CONSTANTS; use twenty_first::shared_math::tip5::STATE_SIZE; +use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; +use triton_opcodes::instruction::Instruction; + +use crate::table::cascade_table::CascadeTable; use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; @@ -258,6 +263,77 @@ impl ExtHashTable { let if_ci_is_absorb_init_and_round_no_is_0_then_state_15_is_0 = if_ci_is_absorb_init_and_round_no_is_0_then_ * state15; + // consistency of the inverse of the highest 2 limbs minus 2^32 - 1 + let one = constant(1); + let two_pow_16 = constant(1 << 16); + let two_pow_32 = constant(1 << 32); + let state_0_hi_limbs_minus_2_pow_32 = two_pow_32.clone() + - one.clone() + - base_row(State0HighestLkIn) * two_pow_16.clone() + - base_row(State0MidHighLkIn); + let state_1_hi_limbs_minus_2_pow_32 = two_pow_32.clone() + - one.clone() + - base_row(State1HighestLkIn) * two_pow_16.clone() + - base_row(State1MidHighLkIn); + let state_2_hi_limbs_minus_2_pow_32 = two_pow_32.clone() + - one.clone() + - base_row(State2HighestLkIn) * two_pow_16.clone() + - base_row(State2MidHighLkIn); + let state_3_hi_limbs_minus_2_pow_32 = two_pow_32.clone() + - one.clone() + - base_row(State3HighestLkIn) * two_pow_16.clone() + - base_row(State3MidHighLkIn); + + let state_0_hi_limbs_inv = base_row(State0Inv); + let state_1_hi_limbs_inv = base_row(State1Inv); + let state_2_hi_limbs_inv = base_row(State2Inv); + let state_3_hi_limbs_inv = base_row(State3Inv); + + let state_0_hi_limbs_are_not_all_1s = + state_0_hi_limbs_minus_2_pow_32.clone() * state_0_hi_limbs_inv.clone() - one.clone(); + let state_1_hi_limbs_are_not_all_1s = + state_1_hi_limbs_minus_2_pow_32.clone() * state_1_hi_limbs_inv.clone() - one.clone(); + let state_2_hi_limbs_are_not_all_1s = + state_2_hi_limbs_minus_2_pow_32.clone() * state_2_hi_limbs_inv.clone() - one.clone(); + let state_3_hi_limbs_are_not_all_1s = + state_3_hi_limbs_minus_2_pow_32.clone() * state_3_hi_limbs_inv.clone() - one; + + let state_0_hi_limbs_inv_is_inv_or_is_zero = + state_0_hi_limbs_are_not_all_1s.clone() * state_0_hi_limbs_inv; + let state_1_hi_limbs_inv_is_inv_or_is_zero = + state_1_hi_limbs_are_not_all_1s.clone() * state_1_hi_limbs_inv; + let state_2_hi_limbs_inv_is_inv_or_is_zero = + state_2_hi_limbs_are_not_all_1s.clone() * state_2_hi_limbs_inv; + let state_3_hi_limbs_inv_is_inv_or_is_zero = + state_3_hi_limbs_are_not_all_1s.clone() * state_3_hi_limbs_inv; + + let state_0_hi_limbs_inv_is_inv_or_state_0_hi_limbs_is_zero = + state_0_hi_limbs_are_not_all_1s.clone() * state_0_hi_limbs_minus_2_pow_32; + let state_1_hi_limbs_inv_is_inv_or_state_1_hi_limbs_is_zero = + state_1_hi_limbs_are_not_all_1s.clone() * state_1_hi_limbs_minus_2_pow_32; + let state_2_hi_limbs_inv_is_inv_or_state_2_hi_limbs_is_zero = + state_2_hi_limbs_are_not_all_1s.clone() * state_2_hi_limbs_minus_2_pow_32; + let state_3_hi_limbs_inv_is_inv_or_state_3_hi_limbs_is_zero = + state_3_hi_limbs_are_not_all_1s.clone() * state_3_hi_limbs_minus_2_pow_32; + + // consistent decomposition into limbs + let state_0_lo_limbs = + base_row(State0MidLowLkIn) * two_pow_16.clone() + base_row(State0LowestLkIn); + let state_1_lo_limbs = + base_row(State1MidLowLkIn) * two_pow_16.clone() + base_row(State1LowestLkIn); + let state_2_lo_limbs = + base_row(State2MidLowLkIn) * two_pow_16.clone() + base_row(State2LowestLkIn); + let state_3_lo_limbs = base_row(State3MidLowLkIn) * two_pow_16 + base_row(State3LowestLkIn); + + let if_state_0_hi_limbs_are_all_1_then_state_0_lo_limbs_are_all_0 = + state_0_hi_limbs_are_not_all_1s * state_0_lo_limbs; + let if_state_1_hi_limbs_are_all_1_then_state_1_lo_limbs_are_all_0 = + state_1_hi_limbs_are_not_all_1s * state_1_lo_limbs; + let if_state_2_hi_limbs_are_all_1_then_state_2_lo_limbs_are_all_0 = + state_2_hi_limbs_are_not_all_1s * state_2_lo_limbs; + let if_state_3_hi_limbs_are_all_1_then_state_3_lo_limbs_are_all_0 = + state_3_hi_limbs_are_not_all_1s * state_3_lo_limbs; + let mut constraints = vec![ if_padding_row_then_ci_is_hash, if_ci_is_hash_and_round_no_is_0_then_state_10_is_1, @@ -272,6 +348,18 @@ impl ExtHashTable { if_ci_is_absorb_init_and_round_no_is_0_then_state_13_is_0, if_ci_is_absorb_init_and_round_no_is_0_then_state_14_is_0, if_ci_is_absorb_init_and_round_no_is_0_then_state_15_is_0, + state_0_hi_limbs_inv_is_inv_or_is_zero, + state_1_hi_limbs_inv_is_inv_or_is_zero, + state_2_hi_limbs_inv_is_inv_or_is_zero, + state_3_hi_limbs_inv_is_inv_or_is_zero, + state_0_hi_limbs_inv_is_inv_or_state_0_hi_limbs_is_zero, + state_1_hi_limbs_inv_is_inv_or_state_1_hi_limbs_is_zero, + state_2_hi_limbs_inv_is_inv_or_state_2_hi_limbs_is_zero, + state_3_hi_limbs_inv_is_inv_or_state_3_hi_limbs_is_zero, + if_state_0_hi_limbs_are_all_1_then_state_0_lo_limbs_are_all_0, + if_state_1_hi_limbs_are_all_1_then_state_1_lo_limbs_are_all_0, + if_state_2_hi_limbs_are_all_1_then_state_2_lo_limbs_are_all_0, + if_state_3_hi_limbs_are_all_1_then_state_3_lo_limbs_are_all_0, ]; for round_constant_column_idx in 0..NUM_ROUND_CONSTANTS { @@ -774,6 +862,151 @@ impl HashTable { BFieldElement::new(mds_matrix_entry as u64) } + /// The round constants for round `round_number` if it is a valid round number in the Tip5 + /// permutation, and the zero vector otherwise. + pub fn tip5_round_constants_by_round_number( + round_number: usize, + ) -> [BFieldElement; NUM_ROUND_CONSTANTS] { + match round_number { + i if i < NUM_ROUNDS => ROUND_CONSTANTS + [NUM_ROUND_CONSTANTS * i..NUM_ROUND_CONSTANTS * (i + 1)] + .try_into() + .unwrap(), + _ => [BFIELD_ZERO; NUM_ROUND_CONSTANTS], + } + } + + /// Given a trace of the Tip5 permutation, construct a trace corresponding to the columns of + /// the Hash Table. This includes + /// + /// - adding the round number + /// - adding the round constants, + /// - decomposing the first [`NUM_SPLIT_AND_LOOKUP`] (== 4) state elements into their + /// constituent limbs, + /// - setting the inverse-or-zero for proving correct limb decomposition, and + /// - adding the looked-up value for each limb. + /// + /// The current instruction is not set. + pub fn convert_to_hash_table_rows( + hash_permutation_trace: [[BFieldElement; STATE_SIZE]; NUM_ROUNDS + 1], + ) -> Array2 { + let mut hash_trace_addendum = Array2::zeros([NUM_ROUNDS + 1, BASE_WIDTH]); + for (round_number, mut row) in hash_trace_addendum.rows_mut().into_iter().enumerate() { + let trace_row = hash_permutation_trace[round_number]; + row[RoundNumber.base_table_index()] = BFieldElement::from(round_number as u64); + + let st_0_raw_limbs = trace_row[0].raw_u16s(); + let st_0_look_in_split = + st_0_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State0LowestLkIn.base_table_index()] = st_0_look_in_split[0]; + row[State0MidLowLkIn.base_table_index()] = st_0_look_in_split[1]; + row[State0MidHighLkIn.base_table_index()] = st_0_look_in_split[2]; + row[State0HighestLkIn.base_table_index()] = st_0_look_in_split[3]; + + let st_0_look_out_split = st_0_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State0LowestLkOut.base_table_index()] = st_0_look_out_split[0]; + row[State0MidLowLkOut.base_table_index()] = st_0_look_out_split[1]; + row[State0MidHighLkOut.base_table_index()] = st_0_look_out_split[2]; + row[State0HighestLkOut.base_table_index()] = st_0_look_out_split[3]; + + let st_1_raw_limbs = trace_row[1].raw_u16s(); + let st_1_look_in_split = + st_1_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State1LowestLkIn.base_table_index()] = st_1_look_in_split[0]; + row[State1MidLowLkIn.base_table_index()] = st_1_look_in_split[1]; + row[State1MidHighLkIn.base_table_index()] = st_1_look_in_split[2]; + row[State1HighestLkIn.base_table_index()] = st_1_look_in_split[3]; + + let st_1_look_out_split = st_1_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State1LowestLkOut.base_table_index()] = st_1_look_out_split[0]; + row[State1MidLowLkOut.base_table_index()] = st_1_look_out_split[1]; + row[State1MidHighLkOut.base_table_index()] = st_1_look_out_split[2]; + row[State1HighestLkOut.base_table_index()] = st_1_look_out_split[3]; + + let st_2_raw_limbs = trace_row[2].raw_u16s(); + let st_2_look_in_split = + st_2_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State2LowestLkIn.base_table_index()] = st_2_look_in_split[0]; + row[State2MidLowLkIn.base_table_index()] = st_2_look_in_split[1]; + row[State2MidHighLkIn.base_table_index()] = st_2_look_in_split[2]; + row[State2HighestLkIn.base_table_index()] = st_2_look_in_split[3]; + + let st_2_look_out_split = st_2_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State2LowestLkOut.base_table_index()] = st_2_look_out_split[0]; + row[State2MidLowLkOut.base_table_index()] = st_2_look_out_split[1]; + row[State2MidHighLkOut.base_table_index()] = st_2_look_out_split[2]; + row[State2HighestLkOut.base_table_index()] = st_2_look_out_split[3]; + + let st_3_raw_limbs = trace_row[3].raw_u16s(); + let st_3_look_in_split = + st_3_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + row[State3LowestLkIn.base_table_index()] = st_3_look_in_split[0]; + row[State3MidLowLkIn.base_table_index()] = st_3_look_in_split[1]; + row[State3MidHighLkIn.base_table_index()] = st_3_look_in_split[2]; + row[State3HighestLkIn.base_table_index()] = st_3_look_in_split[3]; + + let st_3_look_out_split = st_3_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + row[State3LowestLkOut.base_table_index()] = st_3_look_out_split[0]; + row[State3MidLowLkOut.base_table_index()] = st_3_look_out_split[1]; + row[State3MidHighLkOut.base_table_index()] = st_3_look_out_split[2]; + row[State3HighestLkOut.base_table_index()] = st_3_look_out_split[3]; + + row[State4.base_table_index()] = trace_row[4]; + row[State5.base_table_index()] = trace_row[5]; + row[State6.base_table_index()] = trace_row[6]; + row[State7.base_table_index()] = trace_row[7]; + row[State8.base_table_index()] = trace_row[8]; + row[State9.base_table_index()] = trace_row[9]; + row[State10.base_table_index()] = trace_row[10]; + row[State11.base_table_index()] = trace_row[11]; + row[State12.base_table_index()] = trace_row[12]; + row[State13.base_table_index()] = trace_row[13]; + row[State14.base_table_index()] = trace_row[14]; + row[State15.base_table_index()] = trace_row[15]; + + row[State0Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[0]); + row[State1Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[1]); + row[State2Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[2]); + row[State3Inv.base_table_index()] = + Self::inverse_or_zero_of_highest_2_limbs(trace_row[3]); + + let round_constants = Self::tip5_round_constants_by_round_number(round_number); + row[Constant0.base_table_index()] = round_constants[0]; + row[Constant1.base_table_index()] = round_constants[1]; + row[Constant2.base_table_index()] = round_constants[2]; + row[Constant3.base_table_index()] = round_constants[3]; + row[Constant4.base_table_index()] = round_constants[4]; + row[Constant5.base_table_index()] = round_constants[5]; + row[Constant6.base_table_index()] = round_constants[6]; + row[Constant7.base_table_index()] = round_constants[7]; + row[Constant8.base_table_index()] = round_constants[8]; + row[Constant9.base_table_index()] = round_constants[9]; + row[Constant10.base_table_index()] = round_constants[10]; + row[Constant11.base_table_index()] = round_constants[11]; + row[Constant12.base_table_index()] = round_constants[12]; + row[Constant13.base_table_index()] = round_constants[13]; + row[Constant14.base_table_index()] = round_constants[14]; + row[Constant15.base_table_index()] = round_constants[15]; + } + hash_trace_addendum + } + + /// The inverse-or-zero of (2^32 - 1 - 2^16·`highest` - `mid_high`) where `highest` + /// is the most significant limb of the given `state_element`, and `mid_high` the second-most + /// significant limb. + fn inverse_or_zero_of_highest_2_limbs(state_element: BFieldElement) -> BFieldElement { + let limbs = state_element.raw_u16s(); + let highest = limbs[3]; + let mid_high = limbs[2]; + let high_limbs = BFieldElement::from_raw_u16s(&[mid_high, highest, 0, 0]); + let two_pow_32_minus_1 = BFieldElement::new((1 << 32) - 1); + let to_invert = two_pow_32_minus_1 - high_limbs; + to_invert.inverse_or_zero() + } + pub fn fill_trace( hash_table: &mut ArrayViewMut2, aet: &AlgebraicExecutionTrace, @@ -796,6 +1029,21 @@ impl HashTable { hash_table .slice_mut(s![hash_table_length.., RoundNumber.base_table_index()]) .fill(-BFIELD_ONE); + + let inverse_of_high_limbs_plus_1_minus_2_pow_32 = + Self::inverse_or_zero_of_highest_2_limbs(BFIELD_ZERO); + hash_table + .slice_mut(s![hash_table_length.., State0Inv.base_table_index()]) + .fill(inverse_of_high_limbs_plus_1_minus_2_pow_32); + hash_table + .slice_mut(s![hash_table_length.., State1Inv.base_table_index()]) + .fill(inverse_of_high_limbs_plus_1_minus_2_pow_32); + hash_table + .slice_mut(s![hash_table_length.., State2Inv.base_table_index()]) + .fill(inverse_of_high_limbs_plus_1_minus_2_pow_32); + hash_table + .slice_mut(s![hash_table_length.., State3Inv.base_table_index()]) + .fill(inverse_of_high_limbs_plus_1_minus_2_pow_32); } pub fn extend( diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 4086cb878..4286867a9 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -3,7 +3,6 @@ use std::convert::TryInto; use std::fmt::Display; use anyhow::Result; -use itertools::Itertools; use ndarray::s; use ndarray::Array1; use ndarray::Array2; @@ -11,7 +10,6 @@ use ndarray::Axis; use num_traits::One; use num_traits::Zero; use twenty_first::shared_math::b_field_element::BFieldElement; -use twenty_first::shared_math::b_field_element::BFIELD_ZERO; use twenty_first::shared_math::other::log_2_floor; use twenty_first::shared_math::tip5; use twenty_first::shared_math::tip5::Tip5; @@ -33,9 +31,8 @@ use crate::error::vm_fail; use crate::error::InstructionError::InstructionPointerOverflow; use crate::error::InstructionError::*; use crate::op_stack::OpStack; -use crate::table::cascade_table::CascadeTable; use crate::table::hash_table; -use crate::table::hash_table::NUM_ROUND_CONSTANTS; +use crate::table::hash_table::HashTable; use crate::table::processor_table; use crate::table::processor_table::ProcessorTraceRow; use crate::table::table_column::HashBaseTableColumn::*; @@ -967,7 +964,7 @@ impl AlgebraicExecutionTrace { hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], ) { self.increase_lookup_multiplicities(hash_permutation_trace); - let mut hash_trace_addendum = Self::convert_to_hash_table_rows(hash_permutation_trace); + let mut hash_trace_addendum = HashTable::convert_to_hash_table_rows(hash_permutation_trace); hash_trace_addendum .slice_mut(s![.., CI.base_table_index()]) .fill(Instruction::Hash.opcode_b()); @@ -986,7 +983,8 @@ impl AlgebraicExecutionTrace { Instruction::AbsorbInit | Instruction::Absorb | Instruction::Squeeze )); self.increase_lookup_multiplicities(hash_permutation_trace); - let mut sponge_trace_addendum = Self::convert_to_hash_table_rows(hash_permutation_trace); + let mut sponge_trace_addendum = + HashTable::convert_to_hash_table_rows(hash_permutation_trace); sponge_trace_addendum .slice_mut(s![.., CI.base_table_index()]) .fill(instruction.opcode_b()); @@ -1018,149 +1016,6 @@ impl AlgebraicExecutionTrace { } } } - - /// Given a trace of the Tip5 permutation, construct a trace corresponding to the columns of - /// the Hash Table. This includes - /// - /// - adding the round number - /// - adding the round constants, - /// - decomposing the first [`tip5::NUM_SPLIT_AND_LOOKUP`] (== 4) state elements into their - /// constituent limbs, - /// - setting the inverse-or-zero for proving correct limb decomposition, and - /// - adding the looked-up value for each limb. - /// - /// The current instruction is not set. - fn convert_to_hash_table_rows( - hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], - ) -> Array2 { - let mut hash_trace_addendum = Array2::zeros([tip5::NUM_ROUNDS + 1, hash_table::BASE_WIDTH]); - for (round_number, mut row) in hash_trace_addendum.rows_mut().into_iter().enumerate() { - let trace_row = hash_permutation_trace[round_number]; - row[RoundNumber.base_table_index()] = BFieldElement::from(round_number as u64); - - let st_0_raw_limbs = trace_row[0].raw_u16s(); - let st_0_look_in_split = - st_0_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State0LowestLkIn.base_table_index()] = st_0_look_in_split[0]; - row[State0MidLowLkIn.base_table_index()] = st_0_look_in_split[1]; - row[State0MidHighLkIn.base_table_index()] = st_0_look_in_split[2]; - row[State0HighestLkIn.base_table_index()] = st_0_look_in_split[3]; - - let st_0_look_out_split = st_0_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State0LowestLkOut.base_table_index()] = st_0_look_out_split[0]; - row[State0MidLowLkOut.base_table_index()] = st_0_look_out_split[1]; - row[State0MidHighLkOut.base_table_index()] = st_0_look_out_split[2]; - row[State0HighestLkOut.base_table_index()] = st_0_look_out_split[3]; - - let st_1_raw_limbs = trace_row[1].raw_u16s(); - let st_1_look_in_split = - st_1_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State1LowestLkIn.base_table_index()] = st_1_look_in_split[0]; - row[State1MidLowLkIn.base_table_index()] = st_1_look_in_split[1]; - row[State1MidHighLkIn.base_table_index()] = st_1_look_in_split[2]; - row[State1HighestLkIn.base_table_index()] = st_1_look_in_split[3]; - - let st_1_look_out_split = st_1_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State1LowestLkOut.base_table_index()] = st_1_look_out_split[0]; - row[State1MidLowLkOut.base_table_index()] = st_1_look_out_split[1]; - row[State1MidHighLkOut.base_table_index()] = st_1_look_out_split[2]; - row[State1HighestLkOut.base_table_index()] = st_1_look_out_split[3]; - - let st_2_raw_limbs = trace_row[2].raw_u16s(); - let st_2_look_in_split = - st_2_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State2LowestLkIn.base_table_index()] = st_2_look_in_split[0]; - row[State2MidLowLkIn.base_table_index()] = st_2_look_in_split[1]; - row[State2MidHighLkIn.base_table_index()] = st_2_look_in_split[2]; - row[State2HighestLkIn.base_table_index()] = st_2_look_in_split[3]; - - let st_2_look_out_split = st_2_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State2LowestLkOut.base_table_index()] = st_2_look_out_split[0]; - row[State2MidLowLkOut.base_table_index()] = st_2_look_out_split[1]; - row[State2MidHighLkOut.base_table_index()] = st_2_look_out_split[2]; - row[State2HighestLkOut.base_table_index()] = st_2_look_out_split[3]; - - let st_3_raw_limbs = trace_row[3].raw_u16s(); - let st_3_look_in_split = - st_3_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); - row[State3LowestLkIn.base_table_index()] = st_3_look_in_split[0]; - row[State3MidLowLkIn.base_table_index()] = st_3_look_in_split[1]; - row[State3MidHighLkIn.base_table_index()] = st_3_look_in_split[2]; - row[State3HighestLkIn.base_table_index()] = st_3_look_in_split[3]; - - let st_3_look_out_split = st_3_raw_limbs.map(CascadeTable::lookup_16_bit_limb); - row[State3LowestLkOut.base_table_index()] = st_3_look_out_split[0]; - row[State3MidLowLkOut.base_table_index()] = st_3_look_out_split[1]; - row[State3MidHighLkOut.base_table_index()] = st_3_look_out_split[2]; - row[State3HighestLkOut.base_table_index()] = st_3_look_out_split[3]; - - row[State4.base_table_index()] = trace_row[4]; - row[State5.base_table_index()] = trace_row[5]; - row[State6.base_table_index()] = trace_row[6]; - row[State7.base_table_index()] = trace_row[7]; - row[State8.base_table_index()] = trace_row[8]; - row[State9.base_table_index()] = trace_row[9]; - row[State10.base_table_index()] = trace_row[10]; - row[State11.base_table_index()] = trace_row[11]; - row[State12.base_table_index()] = trace_row[12]; - row[State13.base_table_index()] = trace_row[13]; - row[State14.base_table_index()] = trace_row[14]; - row[State15.base_table_index()] = trace_row[15]; - - row[State0Inv.base_table_index()] = - Self::inverse_or_zero_of_highest_2_limbs(trace_row[0]); - row[State1Inv.base_table_index()] = - Self::inverse_or_zero_of_highest_2_limbs(trace_row[1]); - row[State2Inv.base_table_index()] = - Self::inverse_or_zero_of_highest_2_limbs(trace_row[2]); - row[State3Inv.base_table_index()] = - Self::inverse_or_zero_of_highest_2_limbs(trace_row[3]); - - let round_constants = Self::tip5_round_constants_by_round_number(round_number); - row[Constant0.base_table_index()] = round_constants[0]; - row[Constant1.base_table_index()] = round_constants[1]; - row[Constant2.base_table_index()] = round_constants[2]; - row[Constant3.base_table_index()] = round_constants[3]; - row[Constant4.base_table_index()] = round_constants[4]; - row[Constant5.base_table_index()] = round_constants[5]; - row[Constant6.base_table_index()] = round_constants[6]; - row[Constant7.base_table_index()] = round_constants[7]; - row[Constant8.base_table_index()] = round_constants[8]; - row[Constant9.base_table_index()] = round_constants[9]; - row[Constant10.base_table_index()] = round_constants[10]; - row[Constant11.base_table_index()] = round_constants[11]; - row[Constant12.base_table_index()] = round_constants[12]; - row[Constant13.base_table_index()] = round_constants[13]; - row[Constant14.base_table_index()] = round_constants[14]; - row[Constant15.base_table_index()] = round_constants[15]; - } - hash_trace_addendum - } - - /// The round constants for round `round_number` if it is a valid round number in the Tip5 - /// permutation, and the zero vector otherwise. - fn tip5_round_constants_by_round_number( - round_number: usize, - ) -> [BFieldElement; NUM_ROUND_CONSTANTS] { - match round_number { - i if i < tip5::NUM_ROUNDS => tip5::ROUND_CONSTANTS - [NUM_ROUND_CONSTANTS * i..NUM_ROUND_CONSTANTS * (i + 1)] - .try_into() - .unwrap(), - _ => [BFIELD_ZERO; NUM_ROUND_CONSTANTS], - } - } - - /// The inverse-or-zero of (`mid_high` + (`highest` << 16) - (1 << 32) + 1) where `highest` - /// is the most significant limb of the given `state_element`, and `mid_high` the second-most - /// significant limb. - fn inverse_or_zero_of_highest_2_limbs(state_element: BFieldElement) -> BFieldElement { - let limbs = state_element.raw_u16s().map(|limb| limb as u64); - let highest = limbs[3]; - let mid_high = limbs[2]; - let to_invert = mid_high + (highest << 16) - (1 << 32) + 1; - BFieldElement::from_raw_u64(to_invert).inverse_or_zero() - } } #[cfg(test)] From f89ea44375f71d1e997d5f2d5a5c0a629bd80ca7 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 09:20:06 +0100 Subject: [PATCH 29/49] correctly increase lookup multiplicities of Lookup Table --- triton-vm/src/vm.rs | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 4286867a9..f8222b5a7 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -1,3 +1,4 @@ +use std::collections::hash_map::Entry::*; use std::collections::HashMap; use std::convert::TryInto; use std::fmt::Display; @@ -1001,17 +1002,21 @@ impl AlgebraicExecutionTrace { &mut self, hash_permutation_trace: [[BFieldElement; tip5::STATE_SIZE]; tip5::NUM_ROUNDS + 1], ) { + // The last row in the trace is the permutation's result, meaning that no lookups are + // performed on it. Therefore, we skip it. for row in hash_permutation_trace.iter().rev().skip(1) { for state_element in row[0..tip5::NUM_SPLIT_AND_LOOKUP].iter() { for limb in state_element.raw_u16s() { - let limb_lo = limb & 0xff; - let limb_hi = (limb >> 8) & 0xff; - self.lookup_table_lookup_multiplicities[limb_lo as usize] += 1; - self.lookup_table_lookup_multiplicities[limb_hi as usize] += 1; - self.cascade_table_lookup_multiplicities - .entry(limb) - .and_modify(|e| *e += 1) - .or_insert(1); + match self.cascade_table_lookup_multiplicities.entry(limb) { + Occupied(mut cascade_table_entry) => *cascade_table_entry.get_mut() += 1, + Vacant(cascade_table_entry) => { + cascade_table_entry.insert(1); + let limb_lo = limb & 0xff; + let limb_hi = (limb >> 8) & 0xff; + self.lookup_table_lookup_multiplicities[limb_lo as usize] += 1; + self.lookup_table_lookup_multiplicities[limb_hi as usize] += 1; + } + } } } } From e57b01ef7f1bf78d64bb1890319f9ed22439fab4 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 09:20:31 +0100 Subject: [PATCH 30/49] add `IsPadding` column to Cascade Table --- triton-vm/src/table/table_column.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index 2a91ed870..381a9e94b 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -274,6 +274,9 @@ pub enum HashExtTableColumn { #[repr(usize)] #[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] pub enum CascadeBaseTableColumn { + /// Indicator for padding rows. + IsPadding, + /// The more significant bits of the lookup input. LookInHi, @@ -294,10 +297,11 @@ pub enum CascadeBaseTableColumn { #[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] pub enum CascadeExtTableColumn { /// The (running sum of the) logarithmic derivative for the Lookup Argument with the Hash Table. - /// In every row, the sum accumulates `LookupMultiplicity / Combo` where `Combo` is the - /// verifier-weighted combination of - /// - `LookInHi · 2^32 + LookInLo`, and - /// - `LookOutHi · 2^32 + LookOutLo`. + /// In every row, the sum accumulates `LookupMultiplicity / (X - Combo)` where `X` is a + /// verifier-supplied challenge and `Combo` is the weighted sum of + /// - `LookInHi · 2^16 + LookInLo`, and + /// - `LookOutHi · 2^16 + LookOutLo` + /// with weights supplied by the verifier. HashTableServerLogDerivative, /// The (running sum of the) logarithmic derivative for the Lookup Argument with the Lookup From 6e813f62c719276fb08de146794a81f42fb08d2b Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 09:20:57 +0100 Subject: [PATCH 31/49] remove superfluous `.clone()` --- triton-vm/src/table/hash_table.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 43e7738bd..4217d11f8 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -279,7 +279,7 @@ impl ExtHashTable { - one.clone() - base_row(State2HighestLkIn) * two_pow_16.clone() - base_row(State2MidHighLkIn); - let state_3_hi_limbs_minus_2_pow_32 = two_pow_32.clone() + let state_3_hi_limbs_minus_2_pow_32 = two_pow_32 - one.clone() - base_row(State3HighestLkIn) * two_pow_16.clone() - base_row(State3MidHighLkIn); From 145ce3159659a2dd252b49cb4e7432b57b8a7cdf Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 10:13:01 +0100 Subject: [PATCH 32/49] add new tables to sanity test --- triton-vm/src/stark.rs | 98 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/triton-vm/src/stark.rs b/triton-vm/src/stark.rs index 83e7bc17b..93ab73f46 100644 --- a/triton-vm/src/stark.rs +++ b/triton-vm/src/stark.rs @@ -861,6 +861,7 @@ pub(crate) mod triton_stark_tests { use triton_opcodes::program::Program; use crate::shared_tests::*; + use crate::table::cascade_table::ExtCascadeTable; use crate::table::challenges::ChallengeId::StandardInputIndeterminate; use crate::table::challenges::ChallengeId::StandardInputTerminal; use crate::table::challenges::ChallengeId::StandardOutputIndeterminate; @@ -872,6 +873,7 @@ pub(crate) mod triton_stark_tests { use crate::table::extension_table::Quotientable; use crate::table::hash_table::ExtHashTable; use crate::table::jump_stack_table::ExtJumpStackTable; + use crate::table::lookup_table::ExtLookupTable; use crate::table::master_table::all_degrees_with_origin; use crate::table::master_table::MasterExtTable; use crate::table::master_table::TableId::ProcessorTable; @@ -1338,6 +1340,22 @@ pub(crate) mod triton_stark_tests { ExtHashTable::num_initial_quotients(), ExtHashTable::initial_quotient_degree_bounds(id).len() ); + assert_eq!( + ExtCascadeTable::num_initial_quotients(), + ExtCascadeTable::evaluate_initial_constraints(br, er, &challenges).len(), + ); + assert_eq!( + ExtCascadeTable::num_initial_quotients(), + ExtCascadeTable::initial_quotient_degree_bounds(id).len() + ); + assert_eq!( + ExtLookupTable::num_initial_quotients(), + ExtLookupTable::evaluate_initial_constraints(br, er, &challenges).len(), + ); + assert_eq!( + ExtLookupTable::num_initial_quotients(), + ExtLookupTable::initial_quotient_degree_bounds(id).len() + ); assert_eq!( ExtU32Table::num_initial_quotients(), ExtU32Table::evaluate_initial_constraints(br, er, &challenges).len(), @@ -1346,6 +1364,14 @@ pub(crate) mod triton_stark_tests { ExtU32Table::num_initial_quotients(), ExtU32Table::initial_quotient_degree_bounds(id).len() ); + assert_eq!( + GrandCrossTableArg::num_initial_quotients(), + GrandCrossTableArg::evaluate_initial_constraints(br, er, &challenges).len(), + ); + assert_eq!( + GrandCrossTableArg::num_initial_quotients(), + GrandCrossTableArg::initial_quotient_degree_bounds(id).len() + ); assert_eq!( ExtProgramTable::num_consistency_quotients(), @@ -1395,6 +1421,22 @@ pub(crate) mod triton_stark_tests { ExtHashTable::num_consistency_quotients(), ExtHashTable::consistency_quotient_degree_bounds(id, ph).len() ); + assert_eq!( + ExtCascadeTable::num_consistency_quotients(), + ExtCascadeTable::evaluate_consistency_constraints(br, er, &challenges).len(), + ); + assert_eq!( + ExtCascadeTable::num_consistency_quotients(), + ExtCascadeTable::consistency_quotient_degree_bounds(id, ph).len() + ); + assert_eq!( + ExtLookupTable::num_consistency_quotients(), + ExtLookupTable::evaluate_consistency_constraints(br, er, &challenges).len(), + ); + assert_eq!( + ExtLookupTable::num_consistency_quotients(), + ExtLookupTable::consistency_quotient_degree_bounds(id, ph).len() + ); assert_eq!( ExtU32Table::num_consistency_quotients(), ExtU32Table::evaluate_consistency_constraints(br, er, &challenges).len(), @@ -1403,6 +1445,14 @@ pub(crate) mod triton_stark_tests { ExtU32Table::num_consistency_quotients(), ExtU32Table::consistency_quotient_degree_bounds(id, ph).len() ); + assert_eq!( + GrandCrossTableArg::num_consistency_quotients(), + GrandCrossTableArg::evaluate_consistency_constraints(br, er, &challenges).len(), + ); + assert_eq!( + GrandCrossTableArg::num_consistency_quotients(), + GrandCrossTableArg::consistency_quotient_degree_bounds(id, ph).len() + ); assert_eq!( ExtProgramTable::num_transition_quotients(), @@ -1452,6 +1502,22 @@ pub(crate) mod triton_stark_tests { ExtHashTable::num_transition_quotients(), ExtHashTable::transition_quotient_degree_bounds(id, ph).len() ); + assert_eq!( + ExtCascadeTable::num_transition_quotients(), + ExtCascadeTable::evaluate_transition_constraints(br, er, br, er, &challenges).len(), + ); + assert_eq!( + ExtCascadeTable::num_transition_quotients(), + ExtCascadeTable::transition_quotient_degree_bounds(id, ph).len() + ); + assert_eq!( + ExtLookupTable::num_transition_quotients(), + ExtLookupTable::evaluate_transition_constraints(br, er, br, er, &challenges).len(), + ); + assert_eq!( + ExtLookupTable::num_transition_quotients(), + ExtLookupTable::transition_quotient_degree_bounds(id, ph).len() + ); assert_eq!( ExtU32Table::num_transition_quotients(), ExtU32Table::evaluate_transition_constraints(br, er, br, er, &challenges).len(), @@ -1460,6 +1526,14 @@ pub(crate) mod triton_stark_tests { ExtU32Table::num_transition_quotients(), ExtU32Table::transition_quotient_degree_bounds(id, ph).len() ); + assert_eq!( + GrandCrossTableArg::num_transition_quotients(), + GrandCrossTableArg::evaluate_transition_constraints(br, er, br, er, &challenges).len(), + ); + assert_eq!( + GrandCrossTableArg::num_transition_quotients(), + GrandCrossTableArg::transition_quotient_degree_bounds(id, ph).len() + ); assert_eq!( ExtProgramTable::num_terminal_quotients(), @@ -1509,6 +1583,22 @@ pub(crate) mod triton_stark_tests { ExtHashTable::num_terminal_quotients(), ExtHashTable::terminal_quotient_degree_bounds(id).len() ); + assert_eq!( + ExtCascadeTable::num_terminal_quotients(), + ExtCascadeTable::evaluate_terminal_constraints(br, er, &challenges).len(), + ); + assert_eq!( + ExtCascadeTable::num_terminal_quotients(), + ExtCascadeTable::terminal_quotient_degree_bounds(id).len() + ); + assert_eq!( + ExtLookupTable::num_terminal_quotients(), + ExtLookupTable::evaluate_terminal_constraints(br, er, &challenges).len(), + ); + assert_eq!( + ExtLookupTable::num_terminal_quotients(), + ExtLookupTable::terminal_quotient_degree_bounds(id).len() + ); assert_eq!( ExtU32Table::num_terminal_quotients(), ExtU32Table::evaluate_terminal_constraints(br, er, &challenges).len(), @@ -1517,6 +1607,14 @@ pub(crate) mod triton_stark_tests { ExtU32Table::num_terminal_quotients(), ExtU32Table::terminal_quotient_degree_bounds(id).len() ); + assert_eq!( + GrandCrossTableArg::num_terminal_quotients(), + GrandCrossTableArg::evaluate_terminal_constraints(br, er, &challenges).len(), + ); + assert_eq!( + GrandCrossTableArg::num_terminal_quotients(), + GrandCrossTableArg::terminal_quotient_degree_bounds(id).len() + ); } #[test] From 9702e4411559ef7b00032931365a1ea91aadaa89 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 10:33:38 +0100 Subject: [PATCH 33/49] fill, pad, and extend Cascade Table, link to Lookup Table --- triton-vm/src/table/cascade_table.rs | 68 +++++++++++++++++++-- triton-vm/src/table/challenges.rs | 1 + triton-vm/src/table/cross_table_argument.rs | 7 +++ 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index eeebaffa2..2e4739acc 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -1,17 +1,28 @@ +use ndarray::s; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; +use num_traits::One; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; +use twenty_first::shared_math::b_field_element::BFIELD_ONE; use twenty_first::shared_math::tip5; +use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; +use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; use crate::table::constraint_circuit::ConstraintCircuitMonad; use crate::table::constraint_circuit::DualRowIndicator; use crate::table::constraint_circuit::SingleRowIndicator; +use crate::table::cross_table_argument::CrossTableArg; +use crate::table::cross_table_argument::LookupArg; use crate::table::table_column::CascadeBaseTableColumn; +use crate::table::table_column::CascadeBaseTableColumn::*; use crate::table::table_column::CascadeExtTableColumn; +use crate::table::table_column::CascadeExtTableColumn::*; +use crate::table::table_column::MasterBaseTableColumn; +use crate::table::table_column::MasterExtTableColumn; use crate::vm::AlgebraicExecutionTrace; pub const BASE_WIDTH: usize = CascadeBaseTableColumn::COUNT; @@ -27,12 +38,28 @@ impl CascadeTable { cascade_table: &mut ArrayViewMut2, aet: &AlgebraicExecutionTrace, ) { + for (row_idx, (&to_look_up, &multiplicity)) in + aet.cascade_table_lookup_multiplicities.iter().enumerate() + { + let to_look_up_lo = (to_look_up & 0xff) as u8; + let to_look_up_hi = ((to_look_up >> 8) & 0xff) as u8; + + let mut row = cascade_table.row_mut(row_idx); + row[LookInLo.base_table_index()] = BFieldElement::from_raw_u64(to_look_up_lo as u64); + row[LookInHi.base_table_index()] = BFieldElement::from_raw_u64(to_look_up_hi as u64); + row[LookOutLo.base_table_index()] = Self::lookup_8_bit_limb(to_look_up_lo); + row[LookOutHi.base_table_index()] = Self::lookup_8_bit_limb(to_look_up_hi); + row[LookupMultiplicity.base_table_index()] = BFieldElement::new(multiplicity); + } } pub fn pad_trace( cascade_table: &mut ArrayViewMut2, cascade_table_length: usize, ) { + cascade_table + .slice_mut(s![cascade_table_length.., IsPadding.base_table_index()]) + .fill(BFIELD_ONE); } pub fn extend( @@ -43,16 +70,45 @@ impl CascadeTable { assert_eq!(BASE_WIDTH, base_table.ncols()); assert_eq!(EXT_WIDTH, ext_table.ncols()); assert_eq!(base_table.nrows(), ext_table.nrows()); + + let mut lookup_table_log_derivative = LookupArg::default_initial(); + + let lookup_input_weight = challenges.get_challenge(LookupTableInputWeight); + let lookup_output_weight = challenges.get_challenge(LookupTableOutputWeight); + let lookup_indeterminate = challenges.get_challenge(CascadeLookupIndeterminate); + + for row_idx in 0..base_table.nrows() { + let base_row = base_table.row(row_idx); + let is_padding = base_row[IsPadding.base_table_index()].is_one(); + + if !is_padding { + let compressed_row_lo = lookup_input_weight * base_row[LookInLo.base_table_index()] + + lookup_output_weight * base_row[LookOutLo.base_table_index()]; + let compressed_row_hi = lookup_input_weight * base_row[LookInHi.base_table_index()] + + lookup_output_weight * base_row[LookOutHi.base_table_index()]; + lookup_table_log_derivative += (lookup_indeterminate - compressed_row_lo).inverse(); + lookup_table_log_derivative += (lookup_indeterminate - compressed_row_hi).inverse(); + } + + let mut extension_row = ext_table.row_mut(row_idx); + extension_row[LookupTableClientLogDerivative.ext_table_index()] = + lookup_table_log_derivative; + } } - pub fn lookup_16_bit_limb(to_look_up: u16) -> BFieldElement { - let to_look_up_lo = (to_look_up & 0xff) as usize; - let to_look_up_hi = ((to_look_up >> 8) & 0xff) as usize; - let looked_up_lo = tip5::LOOKUP_TABLE[to_look_up_lo] as u64; - let looked_up_hi = tip5::LOOKUP_TABLE[to_look_up_hi] as u64; - let looked_up = (looked_up_hi << 8) | looked_up_lo; + fn lookup_8_bit_limb(to_look_up: u8) -> BFieldElement { + let looked_up = tip5::LOOKUP_TABLE[to_look_up as usize] as u64; BFieldElement::from_raw_u64(looked_up) } + + pub fn lookup_16_bit_limb(to_look_up: u16) -> BFieldElement { + let to_look_up_lo = (to_look_up & 0xff) as u8; + let to_look_up_hi = ((to_look_up >> 8) & 0xff) as u8; + let looked_up_lo = Self::lookup_8_bit_limb(to_look_up_lo); + let looked_up_hi = Self::lookup_8_bit_limb(to_look_up_hi); + let two_pow_8 = BFieldElement::new(1 << 8); + two_pow_8 * looked_up_hi + looked_up_lo + } } impl ExtCascadeTable { diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 68dc0cfd8..7b9ee4e6f 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -154,6 +154,7 @@ pub enum ChallengeId { HashInputWeight, HashDigestWeight, SpongeWeight, + CascadeToLookupWeight, ProcessorToU32Weight, ClockJumpDifferenceLookupWeight, diff --git a/triton-vm/src/table/cross_table_argument.rs b/triton-vm/src/table/cross_table_argument.rs index 0fa2cfe38..f75b822fa 100644 --- a/triton-vm/src/table/cross_table_argument.rs +++ b/triton-vm/src/table/cross_table_argument.rs @@ -14,8 +14,10 @@ use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; use crate::table::extension_table::Evaluable; use crate::table::extension_table::Quotientable; +use crate::table::table_column::CascadeExtTableColumn; use crate::table::table_column::HashExtTableColumn; use crate::table::table_column::JumpStackExtTableColumn; +use crate::table::table_column::LookupExtTableColumn; use crate::table::table_column::MasterExtTableColumn; use crate::table::table_column::OpStackExtTableColumn; use crate::table::table_column::ProcessorExtTableColumn; @@ -184,6 +186,10 @@ impl Evaluable for GrandCrossTableArg { - ext_row[ProcessorExtTableColumn::HashDigestEvalArg.master_ext_table_index()]; let sponge = ext_row[ProcessorExtTableColumn::SpongeEvalArg.master_ext_table_index()] - ext_row[HashExtTableColumn::SpongeRunningEvaluation.master_ext_table_index()]; + let casade_to_lookup = ext_row + [CascadeExtTableColumn::LookupTableClientLogDerivative.master_ext_table_index()] + - ext_row + [LookupExtTableColumn::CascadeTableServerLogDerivative.master_ext_table_index()]; let processor_to_u32 = ext_row [ProcessorExtTableColumn::U32LookupClientLogDerivative.master_ext_table_index()] - ext_row[U32ExtTableColumn::LookupServerLogDerivative.master_ext_table_index()]; @@ -207,6 +213,7 @@ impl Evaluable for GrandCrossTableArg { + challenges.get_challenge(HashInputWeight) * hash_input + challenges.get_challenge(HashDigestWeight) * hash_digest + challenges.get_challenge(SpongeWeight) * sponge + + challenges.get_challenge(CascadeToLookupWeight) * casade_to_lookup + challenges.get_challenge(ProcessorToU32Weight) * processor_to_u32 + challenges.get_challenge(ClockJumpDifferenceLookupWeight) * clock_jump_difference_lookup; From 53d58a01850428267571cc277ff8b02d6f01666c Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 12:34:36 +0100 Subject: [PATCH 34/49] account for Montgomery representation when building Lookup Table --- triton-vm/src/table/challenges.rs | 2 +- triton-vm/src/table/lookup_table.rs | 114 ++++++---------------------- triton-vm/src/table/table_column.rs | 8 +- 3 files changed, 28 insertions(+), 96 deletions(-) diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 7b9ee4e6f..79e21f9c5 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -238,7 +238,7 @@ impl Challenges { challenges[StandardOutputIndeterminate.index()], ); let lookup_terminal = EvalArg::compute_terminal( - &LOOKUP_TABLE.map(|i| BFieldElement::new(i as u64)), + &LOOKUP_TABLE.map(|i| BFieldElement::from_raw_u64(i as u64)), EvalArg::default_initial(), challenges[LookupTablePublicIndeterminate.index()], ); diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index eea97232d..7e244e196 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -2,7 +2,7 @@ use ndarray::s; use ndarray::Array1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; -use num_traits::Zero; +use num_traits::One; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::b_field_element::BFIELD_ONE; @@ -50,32 +50,19 @@ impl LookupTable { assert!(lookup_table.nrows() >= 1 << 8); // Lookup Table input - let lookup_input = Array1::from_iter((0_u64..1 << 8).map(BFieldElement::new)); + let lookup_input = Array1::from_iter((0_u64..1 << 8).map(BFieldElement::from_raw_u64)); let lookup_input_column = lookup_table.slice_mut(s![..1_usize << 8, LookIn.base_table_index()]); lookup_input.move_into(lookup_input_column); // Lookup Table output let lookup_output = Array1::from_iter( - (0..1 << 8).map(|i| BFieldElement::new(tip5::LOOKUP_TABLE[i] as u64)), + (0..1 << 8).map(|i| BFieldElement::from_raw_u64(tip5::LOOKUP_TABLE[i] as u64)), ); let lookup_output_column = lookup_table.slice_mut(s![..1_usize << 8, LookOut.base_table_index()]); lookup_output.move_into(lookup_output_column); - // Inverse of 2^8 - Lookup Table input - let inverse_of_2_pow_8_minus_lookup_input = Array1::from_iter( - (1_u64..(1 << 8) + 1) - .map(|a| BFieldElement::new(a).inverse()) - .rev(), - ); - let inverse_of_2_pow_8_minus_lookup_input_column = lookup_table.slice_mut(s![ - ..1_usize << 8, - InverseOf2Pow8MinusLookIn.base_table_index() - ]); - inverse_of_2_pow_8_minus_lookup_input - .move_into(inverse_of_2_pow_8_minus_lookup_input_column); - // Lookup Table multiplicities let lookup_multiplicities = Array1::from_iter( aet.lookup_table_lookup_multiplicities @@ -90,8 +77,8 @@ impl LookupTable { // The Lookup Table is always fully populated. let lookup_table_length: usize = 1 << 8; lookup_table - .slice_mut(s![lookup_table_length.., LookIn.base_table_index()]) - .fill(BFieldElement::new(1 << 8)); + .slice_mut(s![lookup_table_length.., IsPadding.base_table_index()]) + .fill(BFIELD_ONE); } pub fn extend( @@ -117,7 +104,7 @@ impl LookupTable { let lookup_input = base_row[LookIn.base_table_index()]; let lookup_output = base_row[LookOut.base_table_index()]; let lookup_multiplicity = base_row[LookupMultiplicity.base_table_index()]; - let is_padding = base_row[InverseOf2Pow8MinusLookIn.base_table_index()].is_zero(); + let is_padding = base_row[IsPadding.base_table_index()].is_one(); if !is_padding { let compressed_row = @@ -185,44 +172,13 @@ impl ExtLookupTable { } pub fn ext_consistency_constraints_as_circuits() -> Vec> { - let circuit_builder = ConstraintCircuitBuilder::new(); - - let one = circuit_builder.b_constant(BFIELD_ONE); - let two_pow_8 = circuit_builder.b_constant(BFieldElement::new(1 << 8)); - - let base_row = |col_id: LookupBaseTableColumn| { - circuit_builder.input(BaseRow(col_id.master_base_table_index())) - }; - - let lookup_input = base_row(LookIn); - let inverse_of_2_pow_8_minus_lookup_input = base_row(InverseOf2Pow8MinusLookIn); - let lookup_multiplicity = base_row(LookupMultiplicity); - - let two_pow_8_minus_lookup_input = two_pow_8 - lookup_input; - let two_pow_8_minus_lookup_input_times_inv_is_1 = two_pow_8_minus_lookup_input.clone() - * inverse_of_2_pow_8_minus_lookup_input.clone() - - one; - let two_pow_8_minus_lookup_input_times_inv_is_1_or_lookup_input_is_two_pow_8 = - two_pow_8_minus_lookup_input_times_inv_is_1.clone() * two_pow_8_minus_lookup_input; - let two_pow_8_minus_lookup_input_times_inv_is_1_or_inv_is_zero = - two_pow_8_minus_lookup_input_times_inv_is_1.clone() - * inverse_of_2_pow_8_minus_lookup_input; - let if_lookup_input_is_256_then_multiplicity_is_0 = - two_pow_8_minus_lookup_input_times_inv_is_1 * lookup_multiplicity; - - let mut constraints = [ - two_pow_8_minus_lookup_input_times_inv_is_1_or_lookup_input_is_two_pow_8, - two_pow_8_minus_lookup_input_times_inv_is_1_or_inv_is_zero, - if_lookup_input_is_256_then_multiplicity_is_0, - ]; - ConstraintCircuitMonad::constant_folding(&mut constraints); - constraints.map(|circuit| circuit.consume()).to_vec() + vec![] } pub fn ext_transition_constraints_as_circuits() -> Vec> { let circuit_builder = ConstraintCircuitBuilder::new(); let one = circuit_builder.b_constant(BFIELD_ONE); - let twofiftysix = circuit_builder.b_constant(BFieldElement::new(1 << 8)); + let one_montgomery = circuit_builder.b_constant(BFieldElement::from_raw_u64(1)); let current_base_row = |col_id: LookupBaseTableColumn| { circuit_builder.input(CurrentBaseRow(col_id.master_base_table_index())) @@ -239,36 +195,26 @@ impl ExtLookupTable { let challenge = |challenge_id: ChallengeId| circuit_builder.challenge(challenge_id); let lookup_input = current_base_row(LookIn); - let inverse_of_2_pow_8_minus_lookup_input = current_base_row(InverseOf2Pow8MinusLookIn); let cascade_table_server_log_derivative = current_ext_row(CascadeTableServerLogDerivative); let public_evaluation_argument = current_ext_row(PublicEvaluationArgument); let lookup_input_next = next_base_row(LookIn); let lookup_output_next = next_base_row(LookOut); let lookup_multiplicity_next = next_base_row(LookupMultiplicity); - let inverse_of_2_pow_8_minus_lookup_input_next = next_base_row(InverseOf2Pow8MinusLookIn); + let is_padding_next = next_base_row(IsPadding); let cascade_table_server_log_derivative_next = next_ext_row(CascadeTableServerLogDerivative); let public_evaluation_argument_next = next_ext_row(PublicEvaluationArgument); - // Lookup Table's input increments by 1 if and only if it is less than 256. - let lookup_input_is_unequal_to_256 = (twofiftysix.clone() - lookup_input.clone()) - * inverse_of_2_pow_8_minus_lookup_input - - one.clone(); - let if_lookup_input_is_equal_to_256_then_lookup_input_next_is_256 = - lookup_input_is_unequal_to_256 * (lookup_input_next.clone() - twofiftysix.clone()); - let if_lookup_input_is_unequal_to_256_then_lookup_input_next_increments_by_1 = - (lookup_input.clone() - twofiftysix.clone()) - * (lookup_input_next.clone() - lookup_input - one.clone()); - let lookup_input_increments_if_and_only_if_less_than_256 = - if_lookup_input_is_equal_to_256_then_lookup_input_next_is_256 - + if_lookup_input_is_unequal_to_256_then_lookup_input_next_increments_by_1; - - // helper variables to determine whether the next row is a padding row - let lookup_input_next_is_unequal_to_256 = (twofiftysix.clone() - lookup_input_next.clone()) - * inverse_of_2_pow_8_minus_lookup_input_next - - one; - let lookup_input_next_is_equal_to_256 = twofiftysix - lookup_input_next.clone(); + // Lookup Table's input increments by 1 if and only if the next row is not a padding row + let if_next_row_is_padding_row_then_lookup_input_next_is_0 = + is_padding_next.clone() * lookup_input_next.clone(); + let if_next_row_is_not_padding_row_then_lookup_input_next_increments_by_1 = (one.clone() + - is_padding_next.clone()) + * (lookup_input_next.clone() - lookup_input - one_montgomery); + let lookup_input_increments_if_and_only_if_next_row_is_not_padding_row = + if_next_row_is_padding_row_then_lookup_input_next_is_0 + + if_next_row_is_not_padding_row_then_lookup_input_next_increments_by_1; // Lookup Argument with Cascade Table let cascade_table_indeterminate = challenge(CascadeLookupIndeterminate); @@ -281,9 +227,8 @@ impl ExtLookupTable { * (cascade_table_indeterminate - compressed_row) - lookup_multiplicity_next; let cascade_table_log_derivative_updates_if_and_only_if_next_row_is_not_padding_row = - lookup_input_next_is_equal_to_256.clone() * cascade_table_log_derivative_updates - + lookup_input_next_is_unequal_to_256.clone() - * cascade_table_log_derivative_remains; + (one.clone() - is_padding_next.clone()) * cascade_table_log_derivative_updates + + is_padding_next.clone() * cascade_table_log_derivative_remains; // public Evaluation Argument let public_indeterminate = challenge(LookupTablePublicIndeterminate); @@ -293,11 +238,11 @@ impl ExtLookupTable { - public_evaluation_argument * public_indeterminate - lookup_output_next; let public_evaluation_argument_updates_if_and_only_if_next_row_is_not_padding_row = - lookup_input_next_is_equal_to_256 * public_evaluation_argument_updates - + lookup_input_next_is_unequal_to_256 * public_evaluation_argument_remains; + (one - is_padding_next.clone()) * public_evaluation_argument_updates + + is_padding_next * public_evaluation_argument_remains; let mut constraints = [ - lookup_input_increments_if_and_only_if_less_than_256, + lookup_input_increments_if_and_only_if_next_row_is_not_padding_row, cascade_table_log_derivative_updates_if_and_only_if_next_row_is_not_padding_row, public_evaluation_argument_updates_if_and_only_if_next_row_is_not_padding_row, ]; @@ -306,17 +251,6 @@ impl ExtLookupTable { } pub fn ext_terminal_constraints_as_circuits() -> Vec> { - let circuit_builder = ConstraintCircuitBuilder::new(); - let twofiftyfive = circuit_builder.b_constant(BFieldElement::new(255)); - let twofiftysix = circuit_builder.b_constant(BFieldElement::new(256)); - - let lookup_input = circuit_builder.input(BaseRow(LookIn.master_base_table_index())); - - let lookup_input_is_255_or_256 = - (lookup_input.clone() - twofiftyfive) * (lookup_input - twofiftysix); - - let mut constraints = [lookup_input_is_255_or_256]; - ConstraintCircuitMonad::constant_folding(&mut constraints); - constraints.map(|circuit| circuit.consume()).to_vec() + vec![] } } diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index 381a9e94b..ff48e122d 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -318,17 +318,15 @@ pub enum CascadeExtTableColumn { #[repr(usize)] #[derive(Display, Debug, Clone, Copy, PartialEq, Eq, EnumIter, EnumCountMacro, Hash)] pub enum LookupBaseTableColumn { + /// Indicator for padding rows. + IsPadding, + /// The lookup input. LookIn, /// The lookup output. LookOut, - /// The inverse-or-zero of 2^8 minus the lookup input. Helps - /// - identifying padding rows, and - /// - enforcing that the padding section is started in the correct row. - InverseOf2Pow8MinusLookIn, - /// The number of times the value is looked up. LookupMultiplicity, } From 55112b0ca13fe5c0c51122e53070b44f80c2cde6 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 14:38:54 +0100 Subject: [PATCH 35/49] link Hash Table and Cascade Table --- triton-vm/src/table/cascade_table.rs | 21 +++- triton-vm/src/table/challenges.rs | 18 ++- triton-vm/src/table/cross_table_argument.rs | 35 ++++++ triton-vm/src/table/hash_table.rs | 128 +++++++++++++++++++- triton-vm/src/table/table_column.rs | 20 +++ 5 files changed, 214 insertions(+), 8 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index 2e4739acc..dbd1f9eb8 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -71,17 +71,34 @@ impl CascadeTable { assert_eq!(EXT_WIDTH, ext_table.ncols()); assert_eq!(base_table.nrows(), ext_table.nrows()); + let mut hash_table_log_derivative = LookupArg::default_initial(); let mut lookup_table_log_derivative = LookupArg::default_initial(); + let two_pow_8 = BFieldElement::new(1 << 8); + + let hash_indeterminate = challenges.get_challenge(HashCascadeLookupIndeterminate); + let hash_input_weight = challenges.get_challenge(HashCascadeLookInWeight); + let hash_output_weight = challenges.get_challenge(HashCascadeLookOutWeight); + + let lookup_indeterminate = challenges.get_challenge(CascadeLookupIndeterminate); let lookup_input_weight = challenges.get_challenge(LookupTableInputWeight); let lookup_output_weight = challenges.get_challenge(LookupTableOutputWeight); - let lookup_indeterminate = challenges.get_challenge(CascadeLookupIndeterminate); for row_idx in 0..base_table.nrows() { let base_row = base_table.row(row_idx); let is_padding = base_row[IsPadding.base_table_index()].is_one(); if !is_padding { + let look_in = two_pow_8 * base_row[LookInHi.base_table_index()] + + base_row[LookInLo.base_table_index()]; + let look_out = two_pow_8 * base_row[LookOutHi.base_table_index()] + + base_row[LookOutLo.base_table_index()]; + let compressed_row_hash = + hash_input_weight * look_in + hash_output_weight * look_out; + let lookup_multiplicity = base_row[LookupMultiplicity.base_table_index()]; + hash_table_log_derivative += + (hash_indeterminate - compressed_row_hash).inverse() * lookup_multiplicity; + let compressed_row_lo = lookup_input_weight * base_row[LookInLo.base_table_index()] + lookup_output_weight * base_row[LookOutLo.base_table_index()]; let compressed_row_hi = lookup_input_weight * base_row[LookInHi.base_table_index()] @@ -91,6 +108,8 @@ impl CascadeTable { } let mut extension_row = ext_table.row_mut(row_idx); + extension_row[HashTableServerLogDerivative.ext_table_index()] = + hash_table_log_derivative; extension_row[LookupTableClientLogDerivative.ext_table_index()] = lookup_table_log_derivative; } diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 79e21f9c5..474f07738 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -123,16 +123,29 @@ pub enum ChallengeId { HashStateWeight14, HashStateWeight15, + /// The indeterminate for the Lookup Argument between the Hash Table and the Cascade Table. + HashCascadeLookupIndeterminate, + + /// A weight for non-linearly combining multiple elements. Applies to + /// - `*LkIn` in the Hash Table, and + /// - `2^16·LookInHi + LookInLo` in the Cascade Table. + HashCascadeLookInWeight, + + /// A weight for non-linearly combining multiple elements. Applies to + /// - `*LkOut` in the Hash Table, and + /// - `2^16·LookOutHi + LookOutLo` in the Cascade Table. + HashCascadeLookOutWeight, + /// The indeterminate for the Lookup Argument between the Cascade Table and the Lookup Table. CascadeLookupIndeterminate, /// A weight for non-linearly combining multiple elements. Applies to - /// - `*LkIn` in the Cascade Table, and + /// - `LkIn*` in the Cascade Table, and /// - `LookIn` in the Lookup Table. LookupTableInputWeight, /// A weight for non-linearly combining multiple elements. Applies to - /// - `*LkOut` in the Cascade Table, and + /// - `LkOut*` in the Cascade Table, and /// - `LookOut` in the Lookup Table. LookupTableOutputWeight, @@ -154,6 +167,7 @@ pub enum ChallengeId { HashInputWeight, HashDigestWeight, SpongeWeight, + HashToCascadeWeight, CascadeToLookupWeight, ProcessorToU32Weight, ClockJumpDifferenceLookupWeight, diff --git a/triton-vm/src/table/cross_table_argument.rs b/triton-vm/src/table/cross_table_argument.rs index f75b822fa..2f84fc27c 100644 --- a/triton-vm/src/table/cross_table_argument.rs +++ b/triton-vm/src/table/cross_table_argument.rs @@ -186,6 +186,40 @@ impl Evaluable for GrandCrossTableArg { - ext_row[ProcessorExtTableColumn::HashDigestEvalArg.master_ext_table_index()]; let sponge = ext_row[ProcessorExtTableColumn::SpongeEvalArg.master_ext_table_index()] - ext_row[HashExtTableColumn::SpongeRunningEvaluation.master_ext_table_index()]; + let hash_to_cascade = ext_row + [CascadeExtTableColumn::HashTableServerLogDerivative.master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState0HighestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState0MidHighClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState0MidLowClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState0LowestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState1HighestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState1MidHighClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState1MidLowClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState1LowestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState2HighestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState2MidHighClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState2MidLowClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState2LowestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState3HighestClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState3MidHighClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState3MidLowClientLogDerivative + .master_ext_table_index()] + - ext_row[HashExtTableColumn::CascadeState3LowestClientLogDerivative + .master_ext_table_index()]; let casade_to_lookup = ext_row [CascadeExtTableColumn::LookupTableClientLogDerivative.master_ext_table_index()] - ext_row @@ -213,6 +247,7 @@ impl Evaluable for GrandCrossTableArg { + challenges.get_challenge(HashInputWeight) * hash_input + challenges.get_challenge(HashDigestWeight) * hash_digest + challenges.get_challenge(SpongeWeight) * sponge + + challenges.get_challenge(HashToCascadeWeight) * hash_to_cascade + challenges.get_challenge(CascadeToLookupWeight) * casade_to_lookup + challenges.get_challenge(ProcessorToU32Weight) * processor_to_u32 + challenges.get_challenge(ClockJumpDifferenceLookupWeight) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 4217d11f8..5dbb8f491 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -34,6 +34,7 @@ use crate::table::constraint_circuit::SingleRowIndicator; use crate::table::constraint_circuit::SingleRowIndicator::*; use crate::table::cross_table_argument::CrossTableArg; use crate::table::cross_table_argument::EvalArg; +use crate::table::cross_table_argument::LookupArg; use crate::table::table_column::HashBaseTableColumn; use crate::table::table_column::HashBaseTableColumn::*; use crate::table::table_column::HashExtTableColumn; @@ -1059,10 +1060,27 @@ impl HashTable { let hash_digest_eval_indeterminate = challenges.get_challenge(HashDigestIndeterminate); let hash_input_eval_indeterminate = challenges.get_challenge(HashInputIndeterminate); let sponge_eval_indeterminate = challenges.get_challenge(SpongeIndeterminate); + let cascade_indeterminate = challenges.get_challenge(HashCascadeLookupIndeterminate); let mut hash_input_running_evaluation = EvalArg::default_initial(); let mut hash_digest_running_evaluation = EvalArg::default_initial(); let mut sponge_running_evaluation = EvalArg::default_initial(); + let mut cascade_state_0_highest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_0_mid_high_log_derivative = LookupArg::default_initial(); + let mut cascade_state_0_mid_low_log_derivative = LookupArg::default_initial(); + let mut cascade_state_0_lowest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_1_highest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_1_mid_high_log_derivative = LookupArg::default_initial(); + let mut cascade_state_1_mid_low_log_derivative = LookupArg::default_initial(); + let mut cascade_state_1_lowest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_2_highest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_2_mid_high_log_derivative = LookupArg::default_initial(); + let mut cascade_state_2_mid_low_log_derivative = LookupArg::default_initial(); + let mut cascade_state_2_lowest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_3_highest_log_derivative = LookupArg::default_initial(); + let mut cascade_state_3_mid_high_log_derivative = LookupArg::default_initial(); + let mut cascade_state_3_mid_low_log_derivative = LookupArg::default_initial(); + let mut cascade_state_3_lowest_log_derivative = LookupArg::default_initial(); let two_pow_16 = BFieldElement::from(1_u64 << 16); let two_pow_32 = BFieldElement::from(1_u64 << 32); @@ -1106,6 +1124,9 @@ impl HashTable { challenges.get_challenge(HashStateWeight9), ]; + let cascade_look_in_weight = challenges.get_challenge(HashCascadeLookInWeight); + let cascade_look_out_weight = challenges.get_challenge(HashCascadeLookOutWeight); + let opcode_hash = Instruction::Hash.opcode_b(); let opcode_absorb_init = Instruction::AbsorbInit.opcode_b(); let opcode_absorb = Instruction::Absorb.opcode_b(); @@ -1114,10 +1135,9 @@ impl HashTable { for row_idx in 0..base_table.nrows() { let row = base_table.row(row_idx); let current_instruction = row[CI.base_table_index()]; + let round_number = row[RoundNumber.base_table_index()]; - if row[RoundNumber.base_table_index()].value() == NUM_ROUNDS as u64 - && current_instruction == opcode_hash - { + if round_number.value() == NUM_ROUNDS as u64 && current_instruction == opcode_hash { // add compressed digest to running evaluation “hash digest” let compressed_hash_digest: XFieldElement = rate_registers(row)[..DIGEST_LENGTH] .iter() @@ -1129,8 +1149,7 @@ impl HashTable { + compressed_hash_digest; } - // all remaining Evaluation Arguments only get updated if the round number is 0 - if row[RoundNumber.base_table_index()].is_zero() { + if round_number.is_zero() { let compressed_row: XFieldElement = state_weights .iter() .zip_eq(rate_registers(row).iter()) @@ -1154,12 +1173,111 @@ impl HashTable { } } + if (0..NUM_ROUNDS as u64).contains(&round_number.value()) { + cascade_state_0_highest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State0HighestLkIn.base_table_index()] + - cascade_look_out_weight * row[State0HighestLkOut.base_table_index()]) + .inverse(); + cascade_state_0_mid_high_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State0MidHighLkIn.base_table_index()] + - cascade_look_out_weight * row[State0MidHighLkOut.base_table_index()]) + .inverse(); + cascade_state_0_mid_low_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State0MidLowLkIn.base_table_index()] + - cascade_look_out_weight * row[State0MidLowLkOut.base_table_index()]) + .inverse(); + cascade_state_0_lowest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State0LowestLkIn.base_table_index()] + - cascade_look_out_weight * row[State0LowestLkOut.base_table_index()]) + .inverse(); + cascade_state_1_highest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State1HighestLkIn.base_table_index()] + - cascade_look_out_weight * row[State1HighestLkOut.base_table_index()]) + .inverse(); + cascade_state_1_mid_high_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State1MidHighLkIn.base_table_index()] + - cascade_look_out_weight * row[State1MidHighLkOut.base_table_index()]) + .inverse(); + cascade_state_1_mid_low_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State1MidLowLkIn.base_table_index()] + - cascade_look_out_weight * row[State1MidLowLkOut.base_table_index()]) + .inverse(); + cascade_state_1_lowest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State1LowestLkIn.base_table_index()] + - cascade_look_out_weight * row[State1LowestLkOut.base_table_index()]) + .inverse(); + cascade_state_2_highest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State2HighestLkIn.base_table_index()] + - cascade_look_out_weight * row[State2HighestLkOut.base_table_index()]) + .inverse(); + cascade_state_2_mid_high_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State2MidHighLkIn.base_table_index()] + - cascade_look_out_weight * row[State2MidHighLkOut.base_table_index()]) + .inverse(); + cascade_state_2_mid_low_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State2MidLowLkIn.base_table_index()] + - cascade_look_out_weight * row[State2MidLowLkOut.base_table_index()]) + .inverse(); + cascade_state_2_lowest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State2LowestLkIn.base_table_index()] + - cascade_look_out_weight * row[State2LowestLkOut.base_table_index()]) + .inverse(); + cascade_state_3_highest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State3HighestLkIn.base_table_index()] + - cascade_look_out_weight * row[State3HighestLkOut.base_table_index()]) + .inverse(); + cascade_state_3_mid_high_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State3MidHighLkIn.base_table_index()] + - cascade_look_out_weight * row[State3MidHighLkOut.base_table_index()]) + .inverse(); + cascade_state_3_mid_low_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State3MidLowLkIn.base_table_index()] + - cascade_look_out_weight * row[State3MidLowLkOut.base_table_index()]) + .inverse(); + cascade_state_3_lowest_log_derivative += (cascade_indeterminate + - cascade_look_in_weight * row[State3LowestLkIn.base_table_index()] + - cascade_look_out_weight * row[State3LowestLkOut.base_table_index()]) + .inverse(); + } + let mut extension_row = ext_table.row_mut(row_idx); extension_row[HashInputRunningEvaluation.ext_table_index()] = hash_input_running_evaluation; extension_row[HashDigestRunningEvaluation.ext_table_index()] = hash_digest_running_evaluation; extension_row[SpongeRunningEvaluation.ext_table_index()] = sponge_running_evaluation; + extension_row[CascadeState0HighestClientLogDerivative.ext_table_index()] = + cascade_state_0_highest_log_derivative; + extension_row[CascadeState0MidHighClientLogDerivative.ext_table_index()] = + cascade_state_0_mid_high_log_derivative; + extension_row[CascadeState0MidLowClientLogDerivative.ext_table_index()] = + cascade_state_0_mid_low_log_derivative; + extension_row[CascadeState0LowestClientLogDerivative.ext_table_index()] = + cascade_state_0_lowest_log_derivative; + extension_row[CascadeState1HighestClientLogDerivative.ext_table_index()] = + cascade_state_1_highest_log_derivative; + extension_row[CascadeState1MidHighClientLogDerivative.ext_table_index()] = + cascade_state_1_mid_high_log_derivative; + extension_row[CascadeState1MidLowClientLogDerivative.ext_table_index()] = + cascade_state_1_mid_low_log_derivative; + extension_row[CascadeState1LowestClientLogDerivative.ext_table_index()] = + cascade_state_1_lowest_log_derivative; + extension_row[CascadeState2HighestClientLogDerivative.ext_table_index()] = + cascade_state_2_highest_log_derivative; + extension_row[CascadeState2MidHighClientLogDerivative.ext_table_index()] = + cascade_state_2_mid_high_log_derivative; + extension_row[CascadeState2MidLowClientLogDerivative.ext_table_index()] = + cascade_state_2_mid_low_log_derivative; + extension_row[CascadeState2LowestClientLogDerivative.ext_table_index()] = + cascade_state_2_lowest_log_derivative; + extension_row[CascadeState3HighestClientLogDerivative.ext_table_index()] = + cascade_state_3_highest_log_derivative; + extension_row[CascadeState3MidHighClientLogDerivative.ext_table_index()] = + cascade_state_3_mid_high_log_derivative; + extension_row[CascadeState3MidLowClientLogDerivative.ext_table_index()] = + cascade_state_3_mid_low_log_derivative; + extension_row[CascadeState3LowestClientLogDerivative.ext_table_index()] = + cascade_state_3_lowest_log_derivative; } } } diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index ff48e122d..40a09e126 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -267,6 +267,26 @@ pub enum HashExtTableColumn { HashDigestRunningEvaluation, SpongeRunningEvaluation, + + CascadeState0HighestClientLogDerivative, + CascadeState0MidHighClientLogDerivative, + CascadeState0MidLowClientLogDerivative, + CascadeState0LowestClientLogDerivative, + + CascadeState1HighestClientLogDerivative, + CascadeState1MidHighClientLogDerivative, + CascadeState1MidLowClientLogDerivative, + CascadeState1LowestClientLogDerivative, + + CascadeState2HighestClientLogDerivative, + CascadeState2MidHighClientLogDerivative, + CascadeState2MidLowClientLogDerivative, + CascadeState2LowestClientLogDerivative, + + CascadeState3HighestClientLogDerivative, + CascadeState3MidHighClientLogDerivative, + CascadeState3MidLowClientLogDerivative, + CascadeState3LowestClientLogDerivative, } // -------- Cascade Table -------- From 96b12773e24307e6b8523e86065a08cd3ec1f926 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 7 Mar 2023 16:44:25 +0100 Subject: [PATCH 36/49] add Cascade Table initial constraints --- triton-vm/src/table/cascade_table.rs | 82 ++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 4 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index dbd1f9eb8..6ec49d5ba 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -9,12 +9,15 @@ use twenty_first::shared_math::tip5; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; +use crate::table::challenges::ChallengeId; use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; +use crate::table::constraint_circuit::ConstraintCircuitBuilder; use crate::table::constraint_circuit::ConstraintCircuitMonad; use crate::table::constraint_circuit::DualRowIndicator; use crate::table::constraint_circuit::SingleRowIndicator; +use crate::table::constraint_circuit::SingleRowIndicator::*; use crate::table::cross_table_argument::CrossTableArg; use crate::table::cross_table_argument::LookupArg; use crate::table::table_column::CascadeBaseTableColumn; @@ -132,26 +135,97 @@ impl CascadeTable { impl ExtCascadeTable { pub fn ext_initial_constraints_as_circuits() -> Vec> { - let mut constraints = []; + let circuit_builder = ConstraintCircuitBuilder::new(); + + let base_row = |col_id: CascadeBaseTableColumn| { + circuit_builder.input(BaseRow(col_id.master_base_table_index())) + }; + let ext_row = |col_id: CascadeExtTableColumn| { + circuit_builder.input(ExtRow(col_id.master_ext_table_index())) + }; + let challenge = |challenge_id: ChallengeId| circuit_builder.challenge(challenge_id); + + let one = circuit_builder.b_constant(BFIELD_ONE); + let two = circuit_builder.b_constant(BFieldElement::new(2)); + let two_pow_8 = circuit_builder.b_constant(BFieldElement::new(1 << 8)); + let lookup_arg_default_initial = circuit_builder.x_constant(LookupArg::default_initial()); + + let is_padding = base_row(IsPadding); + let look_in_hi = base_row(LookInHi); + let look_in_lo = base_row(LookInLo); + let look_out_hi = base_row(LookOutHi); + let look_out_lo = base_row(LookOutLo); + let lookup_multiplicity = base_row(LookupMultiplicity); + let hash_table_server_log_derivative = ext_row(HashTableServerLogDerivative); + let lookup_table_client_log_derivative = ext_row(LookupTableClientLogDerivative); + + let hash_indeterminate = challenge(HashCascadeLookupIndeterminate); + let hash_input_weight = challenge(HashCascadeLookInWeight); + let hash_output_weight = challenge(HashCascadeLookOutWeight); + + let lookup_indeterminate = challenge(CascadeLookupIndeterminate); + let lookup_input_weight = challenge(LookupTableInputWeight); + let lookup_output_weight = challenge(LookupTableOutputWeight); + + // Lookup Argument with Hash Table + let compressed_row_hash = hash_input_weight + * (two_pow_8.clone() * look_in_hi.clone() + look_in_lo.clone()) + + hash_output_weight * (two_pow_8 * look_out_hi.clone() + look_out_lo.clone()); + let hash_table_log_derivative_is_default_initial = + hash_table_server_log_derivative.clone() - lookup_arg_default_initial.clone(); + let hash_table_log_derivative_has_accumulated_first_row = (hash_table_server_log_derivative + - lookup_arg_default_initial.clone()) + * (hash_indeterminate - compressed_row_hash) + - lookup_multiplicity; + let hash_table_log_derivative_is_initialized_correctly = (one.clone() - is_padding.clone()) + * hash_table_log_derivative_has_accumulated_first_row + + is_padding.clone() * hash_table_log_derivative_is_default_initial; + + // Lookup Argument with Lookup Table + let compressed_row_lo = + lookup_input_weight.clone() * look_in_lo + lookup_output_weight.clone() * look_out_lo; + let compressed_row_hi = + lookup_input_weight * look_in_hi + lookup_output_weight * look_out_hi; + let lookup_table_log_derivative_is_default_initial = + lookup_table_client_log_derivative.clone() - lookup_arg_default_initial.clone(); + let lookup_table_log_derivative_has_accumulated_first_row = + (lookup_table_client_log_derivative - lookup_arg_default_initial) + * (lookup_indeterminate.clone() - compressed_row_lo.clone()) + * (lookup_indeterminate.clone() - compressed_row_hi.clone()) + - two * lookup_indeterminate + + compressed_row_lo + + compressed_row_hi; + let lookup_table_log_derivative_is_initialized_correctly = (one - is_padding.clone()) + * lookup_table_log_derivative_has_accumulated_first_row + + is_padding * lookup_table_log_derivative_is_default_initial; + + let mut constraints = [ + hash_table_log_derivative_is_initialized_correctly, + lookup_table_log_derivative_is_initialized_correctly, + ]; ConstraintCircuitMonad::constant_folding(&mut constraints); constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_consistency_constraints_as_circuits() -> Vec> { + // todo: + // - IsPadding is bit let mut constraints = []; ConstraintCircuitMonad::constant_folding(&mut constraints); constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_transition_constraints_as_circuits() -> Vec> { + // todo: + // - if IsPadding is 1, then IsPadding' is 1 + // - HashTableServerLogDerivative + // - LookupTableClientLogDerivative let mut constraints = []; ConstraintCircuitMonad::constant_folding(&mut constraints); constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_terminal_constraints_as_circuits() -> Vec> { - let mut constraints = []; - ConstraintCircuitMonad::constant_folding(&mut constraints); - constraints.map(|circuit| circuit.consume()).to_vec() + vec![] } } From 00966b7b5127624fcc3e3f3d18172d00befdf931 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 8 Mar 2023 08:05:55 +0100 Subject: [PATCH 37/49] add Cascade Table consistency constraints --- triton-vm/src/table/cascade_table.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index 6ec49d5ba..1cb437d15 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -208,9 +208,17 @@ impl ExtCascadeTable { } pub fn ext_consistency_constraints_as_circuits() -> Vec> { - // todo: - // - IsPadding is bit - let mut constraints = []; + let circuit_builder = ConstraintCircuitBuilder::new(); + + let base_row = |col_id: CascadeBaseTableColumn| { + circuit_builder.input(BaseRow(col_id.master_base_table_index())) + }; + + let one = circuit_builder.b_constant(BFIELD_ONE); + let is_padding = base_row(IsPadding); + let is_padding_is_0_or_1 = is_padding.clone() * (one - is_padding); + + let mut constraints = [is_padding_is_0_or_1]; ConstraintCircuitMonad::constant_folding(&mut constraints); constraints.map(|circuit| circuit.consume()).to_vec() } From 18ae812833c4f59741f22dc1e2747912e583764a Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 8 Mar 2023 08:36:27 +0100 Subject: [PATCH 38/49] add Cascade Table transition constraints --- triton-vm/src/table/cascade_table.rs | 90 ++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index 1cb437d15..64b7b4e1f 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -16,6 +16,7 @@ use crate::table::constraint_circuit::ConstraintCircuit; use crate::table::constraint_circuit::ConstraintCircuitBuilder; use crate::table::constraint_circuit::ConstraintCircuitMonad; use crate::table::constraint_circuit::DualRowIndicator; +use crate::table::constraint_circuit::DualRowIndicator::*; use crate::table::constraint_circuit::SingleRowIndicator; use crate::table::constraint_circuit::SingleRowIndicator::*; use crate::table::cross_table_argument::CrossTableArg; @@ -224,11 +225,90 @@ impl ExtCascadeTable { } pub fn ext_transition_constraints_as_circuits() -> Vec> { - // todo: - // - if IsPadding is 1, then IsPadding' is 1 - // - HashTableServerLogDerivative - // - LookupTableClientLogDerivative - let mut constraints = []; + let circuit_builder = ConstraintCircuitBuilder::new(); + let challenge = |c| circuit_builder.challenge(c); + let constant = |c: u64| circuit_builder.b_constant(c.into()); + + let current_base_row = |column_idx: CascadeBaseTableColumn| { + circuit_builder.input(CurrentBaseRow(column_idx.master_base_table_index())) + }; + let next_base_row = |column_idx: CascadeBaseTableColumn| { + circuit_builder.input(NextBaseRow(column_idx.master_base_table_index())) + }; + let current_ext_row = |column_idx: CascadeExtTableColumn| { + circuit_builder.input(CurrentExtRow(column_idx.master_ext_table_index())) + }; + let next_ext_row = |column_idx: CascadeExtTableColumn| { + circuit_builder.input(NextExtRow(column_idx.master_ext_table_index())) + }; + + let one = constant(1); + let two = constant(2); + let two_pow_8 = constant(1 << 8); + + let is_padding = current_base_row(IsPadding); + let hash_table_server_log_derivative = current_ext_row(HashTableServerLogDerivative); + let lookup_table_client_log_derivative = current_ext_row(LookupTableClientLogDerivative); + + let is_padding_next = next_base_row(IsPadding); + let look_in_hi_next = next_base_row(LookInHi); + let look_in_lo_next = next_base_row(LookInLo); + let look_out_hi_next = next_base_row(LookOutHi); + let look_out_lo_next = next_base_row(LookOutLo); + let lookup_multiplicity_next = next_base_row(LookupMultiplicity); + let hash_table_server_log_derivative_next = next_ext_row(HashTableServerLogDerivative); + let lookup_table_client_log_derivative_next = next_ext_row(LookupTableClientLogDerivative); + + let hash_indeterminate = challenge(HashCascadeLookupIndeterminate); + let hash_input_weight = challenge(HashCascadeLookInWeight); + let hash_output_weight = challenge(HashCascadeLookOutWeight); + + let lookup_indeterminate = challenge(CascadeLookupIndeterminate); + let lookup_input_weight = challenge(LookupTableInputWeight); + let lookup_output_weight = challenge(LookupTableOutputWeight); + + // Padding region is contiguous: if current row is padding, then next row is padding. + let if_current_row_is_padding_row_then_next_row_is_padding_row = + is_padding * (one.clone() - is_padding_next.clone()); + + // Lookup Argument with Hash Table + let compressed_next_row_hash = hash_input_weight + * (two_pow_8.clone() * look_in_hi_next.clone() + look_in_lo_next.clone()) + + hash_output_weight + * (two_pow_8 * look_out_hi_next.clone() + look_out_lo_next.clone()); + let hash_table_log_derivative_remains = hash_table_server_log_derivative_next.clone() + - hash_table_server_log_derivative.clone(); + let hash_table_log_derivative_accumulates_next_row = (hash_table_server_log_derivative_next + - hash_table_server_log_derivative) + * (hash_indeterminate - compressed_next_row_hash) + - lookup_multiplicity_next; + let hash_table_log_derivative_updates_correctly = (one.clone() - is_padding_next.clone()) + * hash_table_log_derivative_accumulates_next_row + + is_padding_next.clone() * hash_table_log_derivative_remains; + + // Lookup Argument with Lookup Table + let compressed_row_lo_next = lookup_input_weight.clone() * look_in_lo_next + + lookup_output_weight.clone() * look_out_lo_next; + let compressed_row_hi_next = + lookup_input_weight * look_in_hi_next + lookup_output_weight * look_out_hi_next; + let lookup_table_log_derivative_remains = lookup_table_client_log_derivative_next.clone() + - lookup_table_client_log_derivative.clone(); + let lookup_table_log_derivative_accumulates_next_row = + (lookup_table_client_log_derivative_next - lookup_table_client_log_derivative) + * (lookup_indeterminate.clone() - compressed_row_lo_next.clone()) + * (lookup_indeterminate.clone() - compressed_row_hi_next.clone()) + - two * lookup_indeterminate + + compressed_row_lo_next + + compressed_row_hi_next; + let lookup_table_log_derivative_updates_correctly = (one - is_padding_next.clone()) + * lookup_table_log_derivative_accumulates_next_row + + is_padding_next * lookup_table_log_derivative_remains; + + let mut constraints = [ + if_current_row_is_padding_row_then_next_row_is_padding_row, + hash_table_log_derivative_updates_correctly, + lookup_table_log_derivative_updates_correctly, + ]; ConstraintCircuitMonad::constant_folding(&mut constraints); constraints.map(|circuit| circuit.consume()).to_vec() } From be75cd7dcd015221a97685a85fb87034c609e09e Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 8 Mar 2023 08:36:52 +0100 Subject: [PATCH 39/49] add one Lookup Table transition constraint --- triton-vm/src/table/lookup_table.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index 7e244e196..e1473b945 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -195,6 +195,7 @@ impl ExtLookupTable { let challenge = |challenge_id: ChallengeId| circuit_builder.challenge(challenge_id); let lookup_input = current_base_row(LookIn); + let is_padding = current_base_row(IsPadding); let cascade_table_server_log_derivative = current_ext_row(CascadeTableServerLogDerivative); let public_evaluation_argument = current_ext_row(PublicEvaluationArgument); @@ -206,6 +207,11 @@ impl ExtLookupTable { next_ext_row(CascadeTableServerLogDerivative); let public_evaluation_argument_next = next_ext_row(PublicEvaluationArgument); + // Padding section is contiguous: if the current row is a padding row, then the next row + // is also a padding row. + let if_current_row_is_padding_row_then_next_row_is_padding_row = + is_padding * (one.clone() - is_padding_next.clone()); + // Lookup Table's input increments by 1 if and only if the next row is not a padding row let if_next_row_is_padding_row_then_lookup_input_next_is_0 = is_padding_next.clone() * lookup_input_next.clone(); @@ -242,6 +248,7 @@ impl ExtLookupTable { + is_padding_next * public_evaluation_argument_remains; let mut constraints = [ + if_current_row_is_padding_row_then_next_row_is_padding_row, lookup_input_increments_if_and_only_if_next_row_is_not_padding_row, cascade_table_log_derivative_updates_if_and_only_if_next_row_is_not_padding_row, public_evaluation_argument_updates_if_and_only_if_next_row_is_not_padding_row, From c5f05f23c4fc95bea43682497308be953c9c1d22 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 8 Mar 2023 09:30:19 +0100 Subject: [PATCH 40/49] add remaining Hash Table transition constraints --- triton-vm/src/table/hash_table.rs | 151 ++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 5dbb8f491..ecd3002cb 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -682,6 +682,7 @@ impl ExtHashTable { running_evaluation_hash_digest_is_updated_correctly, running_evaluation_sponge_is_updated_correctly, ], + Self::all_cascade_log_derivative_update_circuits(&circuit_builder).to_vec(), ] .concat(); @@ -847,6 +848,156 @@ impl ExtHashTable { (state_next, hash_function_round_correctly_performs_update) } + fn cascade_log_derivative_update_circuit( + circuit_builder: &ConstraintCircuitBuilder, + look_in_column: HashBaseTableColumn, + look_out_column: HashBaseTableColumn, + cascade_log_derivative_column: HashExtTableColumn, + ) -> ConstraintCircuitMonad { + let challenge = |c| circuit_builder.challenge(c); + let constant = |c: u64| circuit_builder.b_constant(c.into()); + let next_base_row = |column_idx: HashBaseTableColumn| { + circuit_builder.input(NextBaseRow(column_idx.master_base_table_index())) + }; + let current_ext_row = |column_idx: HashExtTableColumn| { + circuit_builder.input(CurrentExtRow(column_idx.master_ext_table_index())) + }; + let next_ext_row = |column_idx: HashExtTableColumn| { + circuit_builder.input(NextExtRow(column_idx.master_ext_table_index())) + }; + + let cascade_indeterminate = challenge(HashCascadeLookupIndeterminate); + let look_in_weight = challenge(HashCascadeLookInWeight); + let look_out_weight = challenge(HashCascadeLookOutWeight); + + let round_number_next = next_base_row(RoundNumber); + let cascade_log_derivative = current_ext_row(cascade_log_derivative_column); + let cascade_log_derivative_next = next_ext_row(cascade_log_derivative_column); + + let compressed_row_state_0_highest_next = look_in_weight * next_base_row(look_in_column) + + look_out_weight * next_base_row(look_out_column); + + let cascade_log_derivative_remains = + cascade_log_derivative_next.clone() - cascade_log_derivative.clone(); + let cascade_log_derivative_updates = (cascade_log_derivative_next - cascade_log_derivative) + * (cascade_indeterminate - compressed_row_state_0_highest_next) + - constant(1); + + let round_number_next_is_neg_1_or_5 = (round_number_next.clone() + constant(1)) + * (round_number_next.clone() - constant(NUM_ROUNDS as u64)); + let round_number_next_is_0_through_4 = round_number_next.clone() + * (round_number_next.clone() - constant(1)) + * (round_number_next.clone() - constant(2)) + * (round_number_next.clone() - constant(3)) + * (round_number_next - constant(4)); + + round_number_next_is_neg_1_or_5 * cascade_log_derivative_updates + + round_number_next_is_0_through_4 * cascade_log_derivative_remains + } + + fn all_cascade_log_derivative_update_circuits( + circuit_builder: &ConstraintCircuitBuilder, + ) -> [ConstraintCircuitMonad; 4 * NUM_SPLIT_AND_LOOKUP] { + [ + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State0HighestLkIn, + State0HighestLkOut, + CascadeState0HighestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State0MidHighLkIn, + State0MidHighLkOut, + CascadeState0MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State0MidLowLkIn, + State0MidLowLkOut, + CascadeState0MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State0LowestLkIn, + State0LowestLkOut, + CascadeState0LowestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State1HighestLkIn, + State1HighestLkOut, + CascadeState1HighestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State1MidHighLkIn, + State1MidHighLkOut, + CascadeState1MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State1MidLowLkIn, + State1MidLowLkOut, + CascadeState1MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State1LowestLkIn, + State1LowestLkOut, + CascadeState1LowestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State2HighestLkIn, + State2HighestLkOut, + CascadeState2HighestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State2MidHighLkIn, + State2MidHighLkOut, + CascadeState2MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State2MidLowLkIn, + State2MidLowLkOut, + CascadeState2MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State2LowestLkIn, + State2LowestLkOut, + CascadeState2LowestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State3HighestLkIn, + State3HighestLkOut, + CascadeState3HighestClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State3MidHighLkIn, + State3MidHighLkOut, + CascadeState3MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State3MidLowLkIn, + State3MidLowLkOut, + CascadeState3MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_update_circuit( + circuit_builder, + State3LowestLkIn, + State3LowestLkOut, + CascadeState3LowestClientLogDerivative, + ), + ] + } + pub fn ext_terminal_constraints_as_circuits() -> Vec> { // no more constraints vec![] From 2f8b50f91332271ba2c15addab358a09ab78ae6c Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 8 Mar 2023 13:56:57 +0100 Subject: [PATCH 41/49] =?UTF-8?q?use=20correct=20split-and-lookup=20S-Box:?= =?UTF-8?q?=20account=20for=20Montgomery=20factor=20=E2=80=9CR=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- triton-vm/src/table/cascade_table.rs | 6 +- triton-vm/src/table/challenges.rs | 2 +- triton-vm/src/table/hash_table.rs | 355 ++++++++++++++++++--------- triton-vm/src/table/lookup_table.rs | 7 +- triton-vm/src/vm.rs | 4 +- 5 files changed, 243 insertions(+), 131 deletions(-) diff --git a/triton-vm/src/table/cascade_table.rs b/triton-vm/src/table/cascade_table.rs index 64b7b4e1f..4be28f3b2 100644 --- a/triton-vm/src/table/cascade_table.rs +++ b/triton-vm/src/table/cascade_table.rs @@ -49,8 +49,8 @@ impl CascadeTable { let to_look_up_hi = ((to_look_up >> 8) & 0xff) as u8; let mut row = cascade_table.row_mut(row_idx); - row[LookInLo.base_table_index()] = BFieldElement::from_raw_u64(to_look_up_lo as u64); - row[LookInHi.base_table_index()] = BFieldElement::from_raw_u64(to_look_up_hi as u64); + row[LookInLo.base_table_index()] = BFieldElement::new(to_look_up_lo as u64); + row[LookInHi.base_table_index()] = BFieldElement::new(to_look_up_hi as u64); row[LookOutLo.base_table_index()] = Self::lookup_8_bit_limb(to_look_up_lo); row[LookOutHi.base_table_index()] = Self::lookup_8_bit_limb(to_look_up_hi); row[LookupMultiplicity.base_table_index()] = BFieldElement::new(multiplicity); @@ -121,7 +121,7 @@ impl CascadeTable { fn lookup_8_bit_limb(to_look_up: u8) -> BFieldElement { let looked_up = tip5::LOOKUP_TABLE[to_look_up as usize] as u64; - BFieldElement::from_raw_u64(looked_up) + BFieldElement::new(looked_up) } pub fn lookup_16_bit_limb(to_look_up: u16) -> BFieldElement { diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 474f07738..32dffb1d3 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -252,7 +252,7 @@ impl Challenges { challenges[StandardOutputIndeterminate.index()], ); let lookup_terminal = EvalArg::compute_terminal( - &LOOKUP_TABLE.map(|i| BFieldElement::from_raw_u64(i as u64)), + &LOOKUP_TABLE.map(|i| BFieldElement::new(i as u64)), EvalArg::default_initial(), challenges[LookupTablePublicIndeterminate.index()], ); diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index ecd3002cb..27b895c63 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -57,11 +57,35 @@ pub struct HashTable {} pub struct ExtHashTable {} impl ExtHashTable { + fn re_compose_16_bit_limbs( + circuit_builder: &ConstraintCircuitBuilder, + highest: ConstraintCircuitMonad, + mid_high: ConstraintCircuitMonad, + mid_low: ConstraintCircuitMonad, + lowest: ConstraintCircuitMonad, + ) -> ConstraintCircuitMonad { + let constant = |c: u64| circuit_builder.b_constant(c.into()); + + // R is the field element congruent to 2^64 modulo p, accounting for native + // representation of field elements in Montgomery form. + // State elements are multiplied by R prior to decomposition into limbs. + // After re-composition from limbs, the result must be multiplied by R_inv. + // See `HashTable::base_field_element_16_bit_limbs` for some more details. + let capital_r = BFieldElement::new(((1_u128 << 64) % BFieldElement::P as u128) as u64); + let capital_r_inv = circuit_builder.b_constant(capital_r.inverse()); + + (highest * constant(1 << 48) + + mid_high * constant(1 << 32) + + mid_low * constant(1 << 16) + + lowest) + * capital_r_inv + } + pub fn ext_initial_constraints_as_circuits() -> Vec> { let circuit_builder = ConstraintCircuitBuilder::new(); let challenge = |c| circuit_builder.challenge(c); - let constant = |c| circuit_builder.b_constant(c); - let one = constant(BFIELD_ONE); + let constant = |c: u64| circuit_builder.b_constant(c.into()); + let b_constant = |c| circuit_builder.b_constant(c); let base_row = |column: HashBaseTableColumn| { circuit_builder.input(BaseRow(column.master_base_table_index())) @@ -78,26 +102,36 @@ impl ExtHashTable { let running_evaluation_hash_digest = ext_row(HashDigestRunningEvaluation); let running_evaluation_sponge = ext_row(SpongeRunningEvaluation); - let two_pow_16 = constant(BFieldElement::new(1_u64 << 16)); - let two_pow_32 = constant(BFieldElement::new(1_u64 << 32)); - let two_pow_48 = constant(BFieldElement::new(1_u64 << 48)); - - let state_0 = base_row(State0HighestLkIn) * two_pow_48.clone() - + base_row(State0MidHighLkIn) * two_pow_32.clone() - + base_row(State0MidLowLkIn) * two_pow_16.clone() - + base_row(State0LowestLkIn); - let state_1 = base_row(State1HighestLkIn) * two_pow_48.clone() - + base_row(State1MidHighLkIn) * two_pow_32.clone() - + base_row(State1MidLowLkIn) * two_pow_16.clone() - + base_row(State1LowestLkIn); - let state_2 = base_row(State2HighestLkIn) * two_pow_48.clone() - + base_row(State2MidHighLkIn) * two_pow_32.clone() - + base_row(State2MidLowLkIn) * two_pow_16.clone() - + base_row(State2LowestLkIn); - let state_3 = base_row(State3HighestLkIn) * two_pow_48 - + base_row(State3MidHighLkIn) * two_pow_32 - + base_row(State3MidLowLkIn) * two_pow_16 - + base_row(State3LowestLkIn); + let one = constant(1); + + let state_0 = Self::re_compose_16_bit_limbs( + &circuit_builder, + base_row(State0HighestLkIn), + base_row(State0MidHighLkIn), + base_row(State0MidLowLkIn), + base_row(State0LowestLkIn), + ); + let state_1 = Self::re_compose_16_bit_limbs( + &circuit_builder, + base_row(State1HighestLkIn), + base_row(State1MidHighLkIn), + base_row(State1MidLowLkIn), + base_row(State1LowestLkIn), + ); + let state_2 = Self::re_compose_16_bit_limbs( + &circuit_builder, + base_row(State2HighestLkIn), + base_row(State2MidHighLkIn), + base_row(State2MidLowLkIn), + base_row(State2LowestLkIn), + ); + let state_3 = Self::re_compose_16_bit_limbs( + &circuit_builder, + base_row(State3HighestLkIn), + base_row(State3MidHighLkIn), + base_row(State3MidLowLkIn), + base_row(State3LowestLkIn), + ); let state = [ state_0, @@ -133,8 +167,8 @@ impl ExtHashTable { let round_number_is_neg_1_or_0 = (round_number.clone() + one.clone()) * round_number.clone(); - let ci_is_hash = ci.clone() - constant(Instruction::Hash.opcode_b()); - let ci_is_absorb_init = ci - constant(Instruction::AbsorbInit.opcode_b()); + let ci_is_hash = ci.clone() - b_constant(Instruction::Hash.opcode_b()); + let ci_is_absorb_init = ci - b_constant(Instruction::AbsorbInit.opcode_b()); let current_instruction_is_absorb_init_or_hash = ci_is_absorb_init.clone() * ci_is_hash.clone(); @@ -164,7 +198,7 @@ impl ExtHashTable { running_evaluation_sponge.clone() - running_evaluation_initial.clone(); let running_evaluation_sponge_has_accumulated_first_row = running_evaluation_sponge - running_evaluation_initial * sponge_indeterminate - - challenge(HashCIWeight) * constant(Instruction::AbsorbInit.opcode_b()) + - challenge(HashCIWeight) * b_constant(Instruction::AbsorbInit.opcode_b()) - compressed_row; let running_evaluation_sponge_absorb_is_initialized_correctly = ci_is_hash * running_evaluation_sponge_has_accumulated_first_row @@ -418,11 +452,12 @@ impl ExtHashTable { let circuit_builder = ConstraintCircuitBuilder::new(); let challenge = |c| circuit_builder.challenge(c); let constant = |c: u64| circuit_builder.b_constant(c.into()); + let b_constant = |c| circuit_builder.b_constant(c); - let opcode_hash = constant(Instruction::Hash.opcode() as u64); - let opcode_absorb_init = constant(Instruction::AbsorbInit.opcode() as u64); - let opcode_absorb = constant(Instruction::Absorb.opcode() as u64); - let opcode_squeeze = constant(Instruction::Squeeze.opcode() as u64); + let opcode_hash = b_constant(Instruction::Hash.opcode_b()); + let opcode_absorb_init = b_constant(Instruction::AbsorbInit.opcode_b()); + let opcode_absorb = b_constant(Instruction::Absorb.opcode_b()); + let opcode_squeeze = b_constant(Instruction::Squeeze.opcode_b()); let current_base_row = |column_idx: HashBaseTableColumn| { circuit_builder.input(CurrentBaseRow(column_idx.master_base_table_index())) @@ -453,26 +488,34 @@ impl ExtHashTable { let running_evaluation_hash_digest_next = next_ext_row(HashDigestRunningEvaluation); let running_evaluation_sponge_next = next_ext_row(SpongeRunningEvaluation); - let two_pow_16 = constant(1 << 16); - let two_pow_32 = constant(1 << 32); - let two_pow_48 = constant(1 << 48); - - let state_0 = current_base_row(State0HighestLkIn) * two_pow_48.clone() - + current_base_row(State0MidHighLkIn) * two_pow_32.clone() - + current_base_row(State0MidLowLkIn) * two_pow_16.clone() - + current_base_row(State0LowestLkIn); - let state_1 = current_base_row(State1HighestLkIn) * two_pow_48.clone() - + current_base_row(State1MidHighLkIn) * two_pow_32.clone() - + current_base_row(State1MidLowLkIn) * two_pow_16.clone() - + current_base_row(State1LowestLkIn); - let state_2 = current_base_row(State2HighestLkIn) * two_pow_48.clone() - + current_base_row(State2MidHighLkIn) * two_pow_32.clone() - + current_base_row(State2MidLowLkIn) * two_pow_16.clone() - + current_base_row(State2LowestLkIn); - let state_3 = current_base_row(State3HighestLkIn) * two_pow_48 - + current_base_row(State3MidHighLkIn) * two_pow_32 - + current_base_row(State3MidLowLkIn) * two_pow_16 - + current_base_row(State3LowestLkIn); + let state_0 = Self::re_compose_16_bit_limbs( + &circuit_builder, + current_base_row(State0HighestLkIn), + current_base_row(State0MidHighLkIn), + current_base_row(State0MidLowLkIn), + current_base_row(State0LowestLkIn), + ); + let state_1 = Self::re_compose_16_bit_limbs( + &circuit_builder, + current_base_row(State1HighestLkIn), + current_base_row(State1MidHighLkIn), + current_base_row(State1MidLowLkIn), + current_base_row(State1LowestLkIn), + ); + let state_2 = Self::re_compose_16_bit_limbs( + &circuit_builder, + current_base_row(State2HighestLkIn), + current_base_row(State2MidHighLkIn), + current_base_row(State2MidLowLkIn), + current_base_row(State2LowestLkIn), + ); + let state_3 = Self::re_compose_16_bit_limbs( + &circuit_builder, + current_base_row(State3HighestLkIn), + current_base_row(State3MidHighLkIn), + current_base_row(State3MidLowLkIn), + current_base_row(State3LowestLkIn), + ); let state_current = [ state_0, @@ -708,26 +751,34 @@ impl ExtHashTable { circuit_builder.input(NextBaseRow(column_idx.master_base_table_index())) }; - let two_pow_16 = constant(1 << 16); - let two_pow_32 = constant(1 << 32); - let two_pow_48 = constant(1 << 48); - - let state_0_after_lookup = current_base_row(State0HighestLkOut) * two_pow_48.clone() - + current_base_row(State0MidHighLkOut) * two_pow_32.clone() - + current_base_row(State0MidLowLkOut) * two_pow_16.clone() - + current_base_row(State0LowestLkOut); - let state_1_after_lookup = current_base_row(State1HighestLkOut) * two_pow_48.clone() - + current_base_row(State1MidHighLkOut) * two_pow_32.clone() - + current_base_row(State1MidLowLkOut) * two_pow_16.clone() - + current_base_row(State1LowestLkOut); - let state_2_after_lookup = current_base_row(State2HighestLkOut) * two_pow_48.clone() - + current_base_row(State2MidHighLkOut) * two_pow_32.clone() - + current_base_row(State2MidLowLkOut) * two_pow_16.clone() - + current_base_row(State2LowestLkOut); - let state_3_after_lookup = current_base_row(State3HighestLkOut) * two_pow_48.clone() - + current_base_row(State3MidHighLkOut) * two_pow_32.clone() - + current_base_row(State3MidLowLkOut) * two_pow_16.clone() - + current_base_row(State3LowestLkOut); + let state_0_after_lookup = Self::re_compose_16_bit_limbs( + circuit_builder, + current_base_row(State0HighestLkOut), + current_base_row(State0MidHighLkOut), + current_base_row(State0MidLowLkOut), + current_base_row(State0LowestLkOut), + ); + let state_1_after_lookup = Self::re_compose_16_bit_limbs( + circuit_builder, + current_base_row(State1HighestLkOut), + current_base_row(State1MidHighLkOut), + current_base_row(State1MidLowLkOut), + current_base_row(State1LowestLkOut), + ); + let state_2_after_lookup = Self::re_compose_16_bit_limbs( + circuit_builder, + current_base_row(State2HighestLkOut), + current_base_row(State2MidHighLkOut), + current_base_row(State2MidLowLkOut), + current_base_row(State2LowestLkOut), + ); + let state_3_after_lookup = Self::re_compose_16_bit_limbs( + circuit_builder, + current_base_row(State3HighestLkOut), + current_base_row(State3MidHighLkOut), + current_base_row(State3MidLowLkOut), + current_base_row(State3LowestLkOut), + ); let state_part_before_power_map: [_; STATE_SIZE - NUM_SPLIT_AND_LOOKUP] = [ State4, State5, State6, State7, State8, State9, State10, State11, State12, State13, @@ -796,22 +847,34 @@ impl ExtHashTable { .try_into() .unwrap(); - let state_0_next = next_base_row(State0HighestLkIn) * two_pow_48.clone() - + next_base_row(State0MidHighLkIn) * two_pow_32.clone() - + next_base_row(State0MidLowLkIn) * two_pow_16.clone() - + next_base_row(State0LowestLkIn); - let state_1_next = next_base_row(State1HighestLkIn) * two_pow_48.clone() - + next_base_row(State1MidHighLkIn) * two_pow_32.clone() - + next_base_row(State1MidLowLkIn) * two_pow_16.clone() - + next_base_row(State1LowestLkIn); - let state_2_next = next_base_row(State2HighestLkIn) * two_pow_48.clone() - + next_base_row(State2MidHighLkIn) * two_pow_32.clone() - + next_base_row(State2MidLowLkIn) * two_pow_16.clone() - + next_base_row(State2LowestLkIn); - let state_3_next = next_base_row(State3HighestLkIn) * two_pow_48 - + next_base_row(State3MidHighLkIn) * two_pow_32 - + next_base_row(State3MidLowLkIn) * two_pow_16 - + next_base_row(State3LowestLkIn); + let state_0_next = Self::re_compose_16_bit_limbs( + circuit_builder, + next_base_row(State0HighestLkIn), + next_base_row(State0MidHighLkIn), + next_base_row(State0MidLowLkIn), + next_base_row(State0LowestLkIn), + ); + let state_1_next = Self::re_compose_16_bit_limbs( + circuit_builder, + next_base_row(State1HighestLkIn), + next_base_row(State1MidHighLkIn), + next_base_row(State1MidLowLkIn), + next_base_row(State1LowestLkIn), + ); + let state_2_next = Self::re_compose_16_bit_limbs( + circuit_builder, + next_base_row(State2HighestLkIn), + next_base_row(State2MidHighLkIn), + next_base_row(State2MidLowLkIn), + next_base_row(State2LowestLkIn), + ); + let state_3_next = Self::re_compose_16_bit_limbs( + circuit_builder, + next_base_row(State3HighestLkIn), + next_base_row(State3MidHighLkIn), + next_base_row(State3MidLowLkIn), + next_base_row(State3LowestLkIn), + ); let state_next = [ state_0_next, @@ -1028,6 +1091,23 @@ impl HashTable { } } + /// Return the 16-bit chunks of the “un-Montgomery'd” representation, in little-endian chunk + /// order. This (basically) translates to the application of `σ(R·x)` for input `x`, which + /// are the first two steps in Tip5's split-and-lookup S-Box. + /// `R` is the Montgomery modulus, _i.e._, `R = 2^64 mod p`. + /// `σ` as described in the paper decomposes the 64-bit input into 8-bit limbs, whereas + /// this method decomposes into 16-bit limbs for arithmetization reasons; the 16-bit limbs + /// are split into 8-bit limbs in the Cascade Table. + /// For a more in-depth explanation of all the necessary steps in the split-and-lookup S-Box, + /// see the [Tip5 paper](https://eprint.iacr.org/2023/107.pdf). + /// + /// Note: this is distinct from the seemingly similar [`raw_u16s`](BFieldElement::raw_u16s). + pub fn base_field_element_into_16_bit_limbs(x: BFieldElement) -> [u16; 4] { + let capital_r = BFieldElement::new(((1_u128 << 64) % BFieldElement::P as u128) as u64); + let r_times_x = capital_r * x; + [0, 16, 32, 48].map(|shift| ((r_times_x.value() >> shift) & 0xffff) as u16) + } + /// Given a trace of the Tip5 permutation, construct a trace corresponding to the columns of /// the Hash Table. This includes /// @@ -1047,57 +1127,53 @@ impl HashTable { let trace_row = hash_permutation_trace[round_number]; row[RoundNumber.base_table_index()] = BFieldElement::from(round_number as u64); - let st_0_raw_limbs = trace_row[0].raw_u16s(); - let st_0_look_in_split = - st_0_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + let st_0_limbs = Self::base_field_element_into_16_bit_limbs(trace_row[0]); + let st_0_look_in_split = st_0_limbs.map(|limb| BFieldElement::new(limb as u64)); row[State0LowestLkIn.base_table_index()] = st_0_look_in_split[0]; row[State0MidLowLkIn.base_table_index()] = st_0_look_in_split[1]; row[State0MidHighLkIn.base_table_index()] = st_0_look_in_split[2]; row[State0HighestLkIn.base_table_index()] = st_0_look_in_split[3]; - let st_0_look_out_split = st_0_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + let st_0_look_out_split = st_0_limbs.map(CascadeTable::lookup_16_bit_limb); row[State0LowestLkOut.base_table_index()] = st_0_look_out_split[0]; row[State0MidLowLkOut.base_table_index()] = st_0_look_out_split[1]; row[State0MidHighLkOut.base_table_index()] = st_0_look_out_split[2]; row[State0HighestLkOut.base_table_index()] = st_0_look_out_split[3]; - let st_1_raw_limbs = trace_row[1].raw_u16s(); - let st_1_look_in_split = - st_1_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + let st_1_limbs = Self::base_field_element_into_16_bit_limbs(trace_row[1]); + let st_1_look_in_split = st_1_limbs.map(|limb| BFieldElement::new(limb as u64)); row[State1LowestLkIn.base_table_index()] = st_1_look_in_split[0]; row[State1MidLowLkIn.base_table_index()] = st_1_look_in_split[1]; row[State1MidHighLkIn.base_table_index()] = st_1_look_in_split[2]; row[State1HighestLkIn.base_table_index()] = st_1_look_in_split[3]; - let st_1_look_out_split = st_1_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + let st_1_look_out_split = st_1_limbs.map(CascadeTable::lookup_16_bit_limb); row[State1LowestLkOut.base_table_index()] = st_1_look_out_split[0]; row[State1MidLowLkOut.base_table_index()] = st_1_look_out_split[1]; row[State1MidHighLkOut.base_table_index()] = st_1_look_out_split[2]; row[State1HighestLkOut.base_table_index()] = st_1_look_out_split[3]; - let st_2_raw_limbs = trace_row[2].raw_u16s(); - let st_2_look_in_split = - st_2_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + let st_2_limbs = Self::base_field_element_into_16_bit_limbs(trace_row[2]); + let st_2_look_in_split = st_2_limbs.map(|limb| BFieldElement::new(limb as u64)); row[State2LowestLkIn.base_table_index()] = st_2_look_in_split[0]; row[State2MidLowLkIn.base_table_index()] = st_2_look_in_split[1]; row[State2MidHighLkIn.base_table_index()] = st_2_look_in_split[2]; row[State2HighestLkIn.base_table_index()] = st_2_look_in_split[3]; - let st_2_look_out_split = st_2_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + let st_2_look_out_split = st_2_limbs.map(CascadeTable::lookup_16_bit_limb); row[State2LowestLkOut.base_table_index()] = st_2_look_out_split[0]; row[State2MidLowLkOut.base_table_index()] = st_2_look_out_split[1]; row[State2MidHighLkOut.base_table_index()] = st_2_look_out_split[2]; row[State2HighestLkOut.base_table_index()] = st_2_look_out_split[3]; - let st_3_raw_limbs = trace_row[3].raw_u16s(); - let st_3_look_in_split = - st_3_raw_limbs.map(|limb| BFieldElement::from_raw_u64(limb as u64)); + let st_3_limbs = Self::base_field_element_into_16_bit_limbs(trace_row[3]); + let st_3_look_in_split = st_3_limbs.map(|limb| BFieldElement::new(limb as u64)); row[State3LowestLkIn.base_table_index()] = st_3_look_in_split[0]; row[State3MidLowLkIn.base_table_index()] = st_3_look_in_split[1]; row[State3MidHighLkIn.base_table_index()] = st_3_look_in_split[2]; row[State3HighestLkIn.base_table_index()] = st_3_look_in_split[3]; - let st_3_look_out_split = st_3_raw_limbs.map(CascadeTable::lookup_16_bit_limb); + let st_3_look_out_split = st_3_limbs.map(CascadeTable::lookup_16_bit_limb); row[State3LowestLkOut.base_table_index()] = st_3_look_out_split[0]; row[State3MidLowLkOut.base_table_index()] = st_3_look_out_split[1]; row[State3MidHighLkOut.base_table_index()] = st_3_look_out_split[2]; @@ -1150,10 +1226,10 @@ impl HashTable { /// is the most significant limb of the given `state_element`, and `mid_high` the second-most /// significant limb. fn inverse_or_zero_of_highest_2_limbs(state_element: BFieldElement) -> BFieldElement { - let limbs = state_element.raw_u16s(); - let highest = limbs[3]; - let mid_high = limbs[2]; - let high_limbs = BFieldElement::from_raw_u16s(&[mid_high, highest, 0, 0]); + let limbs = Self::base_field_element_into_16_bit_limbs(state_element); + let highest = limbs[3] as u64; + let mid_high = limbs[2] as u64; + let high_limbs = BFieldElement::new((highest << 16) + mid_high); let two_pow_32_minus_1 = BFieldElement::new((1 << 32) - 1); let to_invert = two_pow_32_minus_1 - high_limbs; to_invert.inverse_or_zero() @@ -1236,24 +1312,60 @@ impl HashTable { let two_pow_16 = BFieldElement::from(1_u64 << 16); let two_pow_32 = BFieldElement::from(1_u64 << 32); let two_pow_48 = BFieldElement::from(1_u64 << 48); + + // Before decomposition into limbs, the state element is multiplied by R (capital_r). + // After re-composition, we need to divide by R to get the original state element. + // See `Self::base_field_element_16_bit_limbs` for more details. + let capital_r = BFieldElement::new(((1_u128 << 64) % BFieldElement::P as u128) as u64); + let capital_r_inv = capital_r.inverse(); + + let re_compose_state_element = + |row: ArrayView1, + highest: HashBaseTableColumn, + mid_high: HashBaseTableColumn, + mid_low: HashBaseTableColumn, + lowest: HashBaseTableColumn| { + (row[highest.base_table_index()] * two_pow_48 + + row[mid_high.base_table_index()] * two_pow_32 + + row[mid_low.base_table_index()] * two_pow_16 + + row[lowest.base_table_index()]) + * capital_r_inv + }; + let rate_registers = |row: ArrayView1| { + let state_0 = re_compose_state_element( + row, + State0HighestLkIn, + State0MidHighLkIn, + State0MidLowLkIn, + State0LowestLkIn, + ); + let state_1 = re_compose_state_element( + row, + State1HighestLkIn, + State1MidHighLkIn, + State1MidLowLkIn, + State1LowestLkIn, + ); + let state_2 = re_compose_state_element( + row, + State2HighestLkIn, + State2MidHighLkIn, + State2MidLowLkIn, + State2LowestLkIn, + ); + let state_3 = re_compose_state_element( + row, + State3HighestLkIn, + State3MidHighLkIn, + State3MidLowLkIn, + State3LowestLkIn, + ); [ - row[State0HighestLkIn.base_table_index()] * two_pow_48 - + row[State0MidHighLkIn.base_table_index()] * two_pow_32 - + row[State0MidLowLkIn.base_table_index()] * two_pow_16 - + row[State0LowestLkIn.base_table_index()], - row[State1HighestLkIn.base_table_index()] * two_pow_48 - + row[State1MidHighLkIn.base_table_index()] * two_pow_32 - + row[State1MidLowLkIn.base_table_index()] * two_pow_16 - + row[State1LowestLkIn.base_table_index()], - row[State2HighestLkIn.base_table_index()] * two_pow_48 - + row[State2MidHighLkIn.base_table_index()] * two_pow_32 - + row[State2MidLowLkIn.base_table_index()] * two_pow_16 - + row[State2LowestLkIn.base_table_index()], - row[State3HighestLkIn.base_table_index()] * two_pow_48 - + row[State3MidHighLkIn.base_table_index()] * two_pow_32 - + row[State3MidLowLkIn.base_table_index()] * two_pow_16 - + row[State3LowestLkIn.base_table_index()], + state_0, + state_1, + state_2, + state_3, row[State4.base_table_index()], row[State5.base_table_index()], row[State6.base_table_index()], @@ -1262,6 +1374,7 @@ impl HashTable { row[State9.base_table_index()], ] }; + let state_weights = [ challenges.get_challenge(HashStateWeight0), challenges.get_challenge(HashStateWeight1), diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index e1473b945..1ec9cf55f 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -50,14 +50,14 @@ impl LookupTable { assert!(lookup_table.nrows() >= 1 << 8); // Lookup Table input - let lookup_input = Array1::from_iter((0_u64..1 << 8).map(BFieldElement::from_raw_u64)); + let lookup_input = Array1::from_iter((0_u64..1 << 8).map(BFieldElement::new)); let lookup_input_column = lookup_table.slice_mut(s![..1_usize << 8, LookIn.base_table_index()]); lookup_input.move_into(lookup_input_column); // Lookup Table output let lookup_output = Array1::from_iter( - (0..1 << 8).map(|i| BFieldElement::from_raw_u64(tip5::LOOKUP_TABLE[i] as u64)), + (0..1 << 8).map(|i| BFieldElement::new(tip5::LOOKUP_TABLE[i] as u64)), ); let lookup_output_column = lookup_table.slice_mut(s![..1_usize << 8, LookOut.base_table_index()]); @@ -178,7 +178,6 @@ impl ExtLookupTable { pub fn ext_transition_constraints_as_circuits() -> Vec> { let circuit_builder = ConstraintCircuitBuilder::new(); let one = circuit_builder.b_constant(BFIELD_ONE); - let one_montgomery = circuit_builder.b_constant(BFieldElement::from_raw_u64(1)); let current_base_row = |col_id: LookupBaseTableColumn| { circuit_builder.input(CurrentBaseRow(col_id.master_base_table_index())) @@ -217,7 +216,7 @@ impl ExtLookupTable { is_padding_next.clone() * lookup_input_next.clone(); let if_next_row_is_not_padding_row_then_lookup_input_next_increments_by_1 = (one.clone() - is_padding_next.clone()) - * (lookup_input_next.clone() - lookup_input - one_montgomery); + * (lookup_input_next.clone() - lookup_input - one.clone()); let lookup_input_increments_if_and_only_if_next_row_is_not_padding_row = if_next_row_is_padding_row_then_lookup_input_next_is_0 + if_next_row_is_not_padding_row_then_lookup_input_next_increments_by_1; diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index f8222b5a7..5c18f61e4 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -1005,8 +1005,8 @@ impl AlgebraicExecutionTrace { // The last row in the trace is the permutation's result, meaning that no lookups are // performed on it. Therefore, we skip it. for row in hash_permutation_trace.iter().rev().skip(1) { - for state_element in row[0..tip5::NUM_SPLIT_AND_LOOKUP].iter() { - for limb in state_element.raw_u16s() { + for &state_element in row[0..tip5::NUM_SPLIT_AND_LOOKUP].iter() { + for limb in HashTable::base_field_element_into_16_bit_limbs(state_element) { match self.cascade_table_lookup_multiplicities.entry(limb) { Occupied(mut cascade_table_entry) => *cascade_table_entry.get_mut() += 1, Vacant(cascade_table_entry) => { From b28539fef6a393e8a77eb134040de891be0f105f Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 9 Mar 2023 01:48:24 +0100 Subject: [PATCH 42/49] add Hash Table initial constraints for Cascade Table Lookup Arguments --- triton-vm/src/table/hash_table.rs | 156 +++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 5 deletions(-) diff --git a/triton-vm/src/table/hash_table.rs b/triton-vm/src/table/hash_table.rs index 27b895c63..6bab7899f 100644 --- a/triton-vm/src/table/hash_table.rs +++ b/triton-vm/src/table/hash_table.rs @@ -204,16 +204,20 @@ impl ExtHashTable { * running_evaluation_sponge_has_accumulated_first_row + ci_is_absorb_init * running_evaluation_sponge_is_default_initial; - let mut constraints = [ + let mut constraints = vec![ round_number_is_neg_1_or_0, current_instruction_is_absorb_init_or_hash, running_evaluation_hash_input_is_initialized_correctly, running_evaluation_hash_digest_is_default_initial, running_evaluation_sponge_absorb_is_initialized_correctly, ]; - + constraints + .append(&mut Self::all_cascade_log_derivative_init_circuits(&circuit_builder).to_vec()); ConstraintCircuitMonad::constant_folding(&mut constraints); - constraints.map(|circuit| circuit.consume()).to_vec() + constraints + .into_iter() + .map(|circuit| circuit.consume()) + .collect() } fn round_number_deselector( @@ -238,6 +242,148 @@ impl ExtHashTable { .fold(factor_for_neg_1, |a, b| a * b) } + fn cascade_log_derivative_init_circuit( + circuit_builder: &ConstraintCircuitBuilder, + look_in_column: HashBaseTableColumn, + look_out_column: HashBaseTableColumn, + cascade_log_derivative_column: HashExtTableColumn, + ) -> ConstraintCircuitMonad { + let challenge = |c| circuit_builder.challenge(c); + let constant = |c: u64| circuit_builder.b_constant(c.into()); + let base_row = |column_idx: HashBaseTableColumn| { + circuit_builder.input(BaseRow(column_idx.master_base_table_index())) + }; + let ext_row = |column_idx: HashExtTableColumn| { + circuit_builder.input(ExtRow(column_idx.master_ext_table_index())) + }; + + let cascade_indeterminate = challenge(HashCascadeLookupIndeterminate); + let look_in_weight = challenge(HashCascadeLookInWeight); + let look_out_weight = challenge(HashCascadeLookOutWeight); + + let round_number = base_row(RoundNumber); + let cascade_log_derivative = ext_row(cascade_log_derivative_column); + let default_initial = circuit_builder.x_constant(LookupArg::default_initial()); + + let compressed_row = + look_in_weight * base_row(look_in_column) + look_out_weight * base_row(look_out_column); + + let cascade_log_derivative_is_default_initial = + cascade_log_derivative.clone() - default_initial.clone(); + let cascade_log_derivative_updates = (cascade_log_derivative - default_initial) + * (cascade_indeterminate - compressed_row) + - constant(1); + + let round_number_next_is_neg_1 = round_number.clone() + constant(1); + let round_number_next_is_0 = round_number; + + round_number_next_is_neg_1 * cascade_log_derivative_updates + + round_number_next_is_0 * cascade_log_derivative_is_default_initial + } + + fn all_cascade_log_derivative_init_circuits( + circuit_builder: &ConstraintCircuitBuilder, + ) -> [ConstraintCircuitMonad; 4 * NUM_SPLIT_AND_LOOKUP] { + [ + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State0HighestLkIn, + State0HighestLkOut, + CascadeState0HighestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State0MidHighLkIn, + State0MidHighLkOut, + CascadeState0MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State0MidLowLkIn, + State0MidLowLkOut, + CascadeState0MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State0LowestLkIn, + State0LowestLkOut, + CascadeState0LowestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State1HighestLkIn, + State1HighestLkOut, + CascadeState1HighestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State1MidHighLkIn, + State1MidHighLkOut, + CascadeState1MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State1MidLowLkIn, + State1MidLowLkOut, + CascadeState1MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State1LowestLkIn, + State1LowestLkOut, + CascadeState1LowestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State2HighestLkIn, + State2HighestLkOut, + CascadeState2HighestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State2MidHighLkIn, + State2MidHighLkOut, + CascadeState2MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State2MidLowLkIn, + State2MidLowLkOut, + CascadeState2MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State2LowestLkIn, + State2LowestLkOut, + CascadeState2LowestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State3HighestLkIn, + State3HighestLkOut, + CascadeState3HighestClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State3MidHighLkIn, + State3MidHighLkOut, + CascadeState3MidHighClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State3MidLowLkIn, + State3MidLowLkOut, + CascadeState3MidLowClientLogDerivative, + ), + Self::cascade_log_derivative_init_circuit( + circuit_builder, + State3LowestLkIn, + State3LowestLkOut, + CascadeState3LowestClientLogDerivative, + ), + ] + } + pub fn ext_consistency_constraints_as_circuits() -> Vec> { let circuit_builder = ConstraintCircuitBuilder::new(); let constant = |c: u64| circuit_builder.b_constant(c.into()); @@ -937,13 +1083,13 @@ impl ExtHashTable { let cascade_log_derivative = current_ext_row(cascade_log_derivative_column); let cascade_log_derivative_next = next_ext_row(cascade_log_derivative_column); - let compressed_row_state_0_highest_next = look_in_weight * next_base_row(look_in_column) + let compressed_row = look_in_weight * next_base_row(look_in_column) + look_out_weight * next_base_row(look_out_column); let cascade_log_derivative_remains = cascade_log_derivative_next.clone() - cascade_log_derivative.clone(); let cascade_log_derivative_updates = (cascade_log_derivative_next - cascade_log_derivative) - * (cascade_indeterminate - compressed_row_state_0_highest_next) + * (cascade_indeterminate - compressed_row) - constant(1); let round_number_next_is_neg_1_or_5 = (round_number_next.clone() + constant(1)) From da631ecf88e41f8d48aecd7b205b65dd63ebe006 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 9 Mar 2023 15:43:23 +0100 Subject: [PATCH 43/49] update specification for Hash Table --- specification/src/hash-table.md | 47 ++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/specification/src/hash-table.md b/specification/src/hash-table.md index 1b5e01b60..cc1ef9d5d 100644 --- a/specification/src/hash-table.md +++ b/specification/src/hash-table.md @@ -30,6 +30,11 @@ The Hash Table first records all Sponge instructions in the order the processor Then, the Hash Table records all `hash` instructions in the order the processor executed them. This allows the processor to execute `hash` instructions without affecting the Sponge's state. +Note that `state0` through `state3`, corresponding to those states that are being split-and-looked-up in the Tip permutation, are not stored as a single field element. +Instead, four limbs “highest”, “mid high”, “mid low”, and “lowest” are recorded in the Hash Table. +This (basically) corresponds to storing the result of $\sigma(R \cdot \texttt{state_element})$, except that the limbs resulting from $\sigma$ are 16 bit wide, and hence, there are only 4 limbs; +the split into 8-bit limbs happens in the [Cascade Table](cascade-table.md). + ## Base Columns The Hash Table has 66 base columns: @@ -52,13 +57,14 @@ The Hash Table has 19 extension columns: - `RunningEvaluationHashInput` for the Evaluation Argument for copying the input to the hash function from the processor to the hash coprocessor, - `RunningEvaluationHashDigest` for the Evaluation Argument for copying the hash digest from the hash coprocessor to the processor, - `RunningEvaluationSponge` for the Evaluation Argument for copying the 10 next to-be-absorbed elements from the processor to the hash coprocessor or the 10 next squeezed elements from the hash coprocessor to the processor, depending on the instruction, -- 16 columns `state_i_*_LookupClientLogDerivative` (for `highest`, `midhigh`, `midlow`, and `lowest`) establishing correct lookup of the respective limbs in the [Cascade Table](cascade-table.md). +- 16 columns `state_i_limb_LookupClientLogDerivative` (for `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$) establishing correct lookup of the respective limbs in the [Cascade Table](cascade-table.md). ## Padding Each padding row is the all-zero row with the exception of -- `round_no`, which is -1, and -- `CI`, which is the opcode of instruction `hash`. +- `round_no`, which is -1, +- `CI`, which is the opcode of instruction `hash`, and +- `state_i_inv` for `i` $\in \{0, \dots, 3\}$, which is $(2^{32} - 1)^{-1}$. # Arithmetic Intermediate Representation @@ -68,34 +74,43 @@ Both types of challenges are X-field elements, _i.e._, elements of $\mathbb{F}_{ ## Initial Constraints -1. The round number is 0 or 1. +1. The round number is -1 or 0. 1. The current instruction is `hash` or `absorb_init`. -1. If the current instruction is `hash` and the round number is 1, then `RunningEvaluationHashInput` has accumulated the first row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪. +1. If the current instruction is `hash` and the round number is 0, then `RunningEvaluationHashInput` has accumulated the first row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪. Otherwise, `RunningEvaluationHashInput` is 1. 1. `RunningEvaluationHashDigest` is 1. 1. If the current instruction is `absorb_init`, then `RunningEvaluationSponge` has accumulated the first row with respect to challenges 🧅 and 🧄₀ through 🧄₉ and indeterminate 🧽. Otherwise, `RunningEvaluationSponge` is 1. +1. For `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$:
+ If the round number is 0, then `state_i_limb_LookupClientLogDerivative` has accumulated `state_i_limb_lkin` and `state_i_limb_lkout` with respect to challenges 🍒, 🍓 and indeterminate 🧺. + Otherwise, `state_i_limb_LookupClientLogDerivative` is 0. Written as Disjunctive Normal Form, the same constraints can be expressed as: -1. `round_no` is 0 or 1. +1. `round_no` is -1 or 0. 1. `CI` is the opcode of `hash` or of `absorb_init`. -1. (`CI` is the opcode of `absorb_init` or `round_no` is 0 or `RunningEvaluationHashInput` has accumulated the first row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪)
+1. (`CI` is the opcode of `absorb_init` or `round_no` is -1 or `RunningEvaluationHashInput` has accumulated the first row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪)
and (`CI` is the opcode of `hash` or `RunningEvaluationHashInput` is 1)
- and (`round_no` is 1 or `RunningEvaluationHashInput` is 1). + and (`round_no` is 0 or `RunningEvaluationHashInput` is 1). 1. (`CI` is the opcode of `hash` or `RunningEvaluationSponge` has accumulated the first row with respect to challenges 🧅 and 🧄₀ through 🧄₉ and indeterminate 🧽)
and (`CI` is the opcode of `absorb_init` or `RunningEvaluationSponge` is 1). +1. For `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$:
+ (`round_no` is -1 or `state_i_limb_LookupClientLogDerivative` has accumulated the first row)
+ and (`round_no` is 0 or `state_i_limb_LookupClientLogDerivative` is the default initial). ### Initial Constraints as Polynomials -1. `round_no·(round_no - 1)` +1. `(round_no + 1)·round_no` 1. `(CI - opcode(hash))·(CI - opcode(absorb_init))` -1. `(CI - opcode(absorb_init))·round_no·(RunningEvaluationHashInput - 🚪 - 🧄₀·st0 - 🧄₁·st1 - 🧄₂·st2 - 🧄₃·st3 - 🧄₄·st4 - 🧄₅·st5 - 🧄₆·st6 - 🧄₇·st7 - 🧄₈·st8 - 🧄₉·st9)`
+1. `(CI - opcode(absorb_init))·(round_no + 1)·(RunningEvaluationHashInput - 🚪 - 🧄₀·st0 - 🧄₁·st1 - 🧄₂·st2 - 🧄₃·st3 - 🧄₄·st4 - 🧄₅·st5 - 🧄₆·st6 - 🧄₇·st7 - 🧄₈·st8 - 🧄₉·st9)`
`+ (CI - opcode(hash))·(RunningEvaluationHashInput - 1)`
- `+ (round_no - 1)·(RunningEvaluationHashInput - 1)` + `+ round_no·(RunningEvaluationHashInput - 1)` 1. `RunningEvaluationHashDigest - 1` 1. `(CI - opcode(hash))·(RunningEvaluationSponge - 🧽 - 🧅·CI - 🧄₀·st0 - 🧄₁·st1 - 🧄₂·st2 - 🧄₃·st3 - 🧄₄·st4 - 🧄₅·st5 - 🧄₆·st6 - 🧄₇·st7 - 🧄₈·st8 - 🧄₉·st9)`
`+ (CI - opcode(absorb_init))·(RunningEvaluationSponge - 1)` +1. For `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$:
+ `(round_no + 1)·(state_i_limb_LookupClientLogDerivative·(🧺 - 🍒·state_i_limb_lkin - 🍓·state_i_limb_lkout) - 1)`
+ `+ round_no·state_i_limb_LookupClientLogDerivative` ## Consistency Constraints @@ -192,6 +207,9 @@ Written as Disjunctive Normal Form, the same constraints can be expressed as: 1. If the round number is 2, the `state` registers adhere to the rules of applying round 2 of the Tip5 permutation. 1. If the round number is 3, the `state` registers adhere to the rules of applying round 3 of the Tip5 permutation. 1. If the round number is 4, the `state` registers adhere to the rules of applying round 4 of the Tip5 permutation. +1. For `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$:
+ If the next round number is 0, 1, 2, 3, or 4, then `state_i_limb_LookupClientLogDerivative` has accumulated `state_i_limb_lkin'` and `state_i_limb_lkout'` with respect to challenges 🍒, 🍓 and indeterminate 🧺. + Otherwise, `state_i_limb_LookupClientLogDerivative` remains unchanged. Written as Disjunctive Normal Form, the same constraints can be expressed as: @@ -217,6 +235,9 @@ Written as Disjunctive Normal Form, the same constraints can be expressed as: 1. `round_no` is -1 or 0 or 1 or 3 or 4 or 5 or the `state` registers adhere to the rules of applying round 2 of the Tip5 permutation. 1. `round_no` is -1 or 0 or 1 or 2 or 4 or 5 or the `state` registers adhere to the rules of applying round 3 of the Tip5 permutation. 1. `round_no` is -1 or 0 or 1 or 2 or 3 or 5 or the `state` registers adhere to the rules of applying round 4 of the Tip5 permutation. +1. For `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$:
+ (`round_no'` is -1 or 5 or `state_i_limb_LookupClientLogDerivative` has accumulated the next row)
+ and (`round_no` is 0 or 1 or 2 or 3 or 4 or `state_i_limb_LookupClientLogDerivative'` is `state_i_limb_LookupClientLogDerivative`). ### Transition Constraints as Polynomials @@ -241,12 +262,14 @@ Written as Disjunctive Normal Form, the same constraints can be expressed as: `·(RunningEvaluationHashDigest' - 🪟·RunningEvaluationHashDigest - 🧄₀·st0' - 🧄₁·st1' - 🧄₂·st2' - 🧄₃·st3' - 🧄₄·st4')`
`+ (round_no' - 5)·(RunningEvaluationHashDigest' - RunningEvaluationHashDigest)`
`+ (CI' - opcode(hash))·(RunningEvaluationHashDigest' - RunningEvaluationHashDigest)` - 1. 1. `(round_no' + 1)·(round_no' - 1)·(round_no' - 2)·(round_no' - 3)·(round_no' - 4)·(round_no' - 5)`
`·(CI' - opcode(hash))`
`·(RunningEvaluationSponge' - 🧽·RunningEvaluationSponge - 🧅·CI' - 🧄₀·st0' - 🧄₁·st1' - 🧄₂·st2' - 🧄₃·st3' - 🧄₄·st4' - 🧄₅·st5' - 🧄₆·st6' - 🧄₇·st7' - 🧄₈·st8' - 🧄₉·st9')`
1. `+ (round_no' - 0)·(RunningEvaluationSponge' - RunningEvaluationSponge)`
1. `+ (CI' - opcode(absorb_init))·(CI' - opcode(absorb))·(CI' - opcode(squeeze))·(RunningEvaluationSponge' - RunningEvaluationSponge)` +1. For `i` $\in \{0, \dots, 3\}$, `limb` $\in \{$`highest`, `midhigh`, `midlow`, `lowest` $\}$:
+ `(round_no + 1)·(round_no - 5)·((state_i_limb_LookupClientLogDerivative' - state_i_limb_LookupClientLogDerivative)·(🧺 - 🍒·state_i_limb_lkin' - 🍓·state_i_limb_lkout') - 1)`
+ `+ (round_no - 0)·(round_no - 1)·(round_no - 2)·(round_no - 3)·(round_no - 4)·(state_i_limb_LookupClientLogDerivative' - state_i_limb_LookupClientLogDerivative)` 1. The remaining constraints are left as an exercise to the reader. For hints, see the [Tip5 paper](https://eprint.iacr.org/2023/107.pdf). From b3d7829706e895e61d09e5f6e72616e95ca72241 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 9 Mar 2023 16:43:50 +0100 Subject: [PATCH 44/49] add specification for Cascade Table --- specification/src/cascade-table.md | 84 +++++++++++++++++++++++++++++ triton-vm/src/table/table_column.rs | 4 +- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/specification/src/cascade-table.md b/specification/src/cascade-table.md index 1cb43e1db..d2f8a4402 100644 --- a/specification/src/cascade-table.md +++ b/specification/src/cascade-table.md @@ -1 +1,85 @@ # Cascade Table + +The Cascade Table helps arithmetizing the lookups necessary for the split-and-lookup S-box used in the Tip5 permutation. +The main idea is to allow the [Hash Table](hash-table.md) to look up limbs that are 16 bit wide even though the S-box is defined over limbs that are 8 bit wide. +The Cascade Table facilitates the translation of limb widths. +For the actual lookup of the 8-bit limbs, the [Lookup Table](lookup-table.md) is used. +For a more detailed explanation and in-depth analysis, see the [Tip5 paper](https://eprint.iacr.org/2023/107.pdf). + +## Base Columns + +The Cascade Table has 6 base columns: + +| name | description | +|:---------------------|:--------------------------------------------------------------| +| `IsPadding` | Indicator for padding rows. | +| `LookInHi` | The more significant bits of the lookup input. | +| `LookInLo` | The less significant bits of the lookup input. | +| `LookOutHi` | The more significant bits of the lookup output. | +| `LookOutLo` | The less significant bits of the lookup output. | +| `LookupMultiplicity` | The number of times the value is looked up by the Hash Table. | + +## Extension Columns + +The Cascade Table has 2 extension columns: + +- `HashTableServerLogDerivative`, the (running sum of the) logarithmic derivative for the Lookup Argument with the Hash Table. + In every row, the sum accumulates `LookupMultiplicity / (🧺 - Combo)` where 🧺 is a verifier-supplied challenge + and `Combo` is the weighted sum of `LookInHi · 2^8 + LookInLo` and `LookOutHi · 2^8 + LookOutLo` + with weights 🍒 and 🍓 supplied by the verifier. +- `LookupTableClientLogDerivative`, the (running sum of the) logarithmic derivative for the Lookup Argument with the Lookup Table. + In every row, the sum accumulates the two summands + 1. `1 / combo_hi` where `combo_hi` is the verifier-weighted combination of `LookInHi` and `LookOutHi`, and + 1. `1 / combo_lo` where `combo_lo` is the verifier-weighted combination of `LookInLo` and `LookOutLo`. + +## Padding + +Each padding row is the all-zero row with the exception of `IsPadding`, which is 1. + +# Arithmetic Intermediate Representation + +Let all household items (🪥, 🛁, etc.) be challenges, concretely evaluation points, supplied by the verifier. +Let all fruit & vegetables (🥝, 🥥, etc.) be challenges, concretely weights to compress rows, supplied by the verifier. +Both types of challenges are X-field elements, _i.e._, elements of $\mathbb{F}_{p^3}$. + +## Initial Constraints + +1. If the first row is not a padding row, then `HashTableServerLogDerivative` has accumulated the first row with respect to challenges 🍒 and 🍓 and indeterminate 🧺. + Else, `HashTableServerLogDerivative` is 0. +1. If the first row is not a padding row, then `LookupTableClientLogDerivative` has accumulated the first row with respect to challenges 🥦 and 🥒 and indeterminate 🪒. + Else, `LookupTableClientLogDerivative` is 0. + +### Initial Constraints as Polynomials + +1. `(1 - IsPadding)·(HashTableServerLogDerivative·(🧺 - 🍒·(2^8·LookInHi + LookInLo) - 🍓·(2^8 · LookOutHi + LookOutLo)) - LookupMultiplicity)`
+ `+ IsPadding · HashTableServerLogDerivative` +1. `(1 - IsPadding)·(LookupTableClientLogDerivative·(🪒 - 🥦·LookInLo - 🥒·LookOutLo)·(🪒 - 🥦·LookInHi - 🥒·LookOutHi) - 2·🪒 + 🥦·(LookInLo + LookInHi) + 🥒·(LookOutLo + LookOutHi))`
+ `+ IsPadding·LookupTableClientLogDerivative` + +## Consistency Constraints + +1. `IsPadding` is 0 or 1. + +### Consistency Constraints as Polynomials + +1. `IsPadding·(1 - IsPadding)` + +## Transition Constraints + +1. If the current row is a padding row, then the next row is a padding row. +1. If the next row is not a padding row, then `HashTableServerLogDerivative` accumulates the next row with respect to challenges 🍒 and 🍓 and indeterminate 🧺. + Else, `HashTableServerLogDerivative` remains unchanged. +1. If the next row is not a padding row, then `LookupTableClientLogDerivative` accumulates the next row with respect to challenges 🥦 and 🥒 and indeterminate 🪒. + Else, `LookupTableClientLogDerivative` remains unchanged. + +### Transition Constraints as Polynomials + +1. `IsPadding·(1 - IsPadding')` +1. `(1 - IsPadding')·((HashTableServerLogDerivative' - HashTableServerLogDerivative)·(🧺 - 🍒·(2^8·LookInHi' + LookInLo') - 🍓·(2^8 · LookOutHi' + LookOutLo')) - LookupMultiplicity')`
+ `+ IsPadding'·(HashTableServerLogDerivative' - HashTableServerLogDerivative)` +1. `(1 - IsPadding')·((LookupTableClientLogDerivative' - LookupTableClientLogDerivative)·(🪒 - 🥦·LookInLo' - 🥒·LookOutLo')·(🪒 - 🥦·LookInHi' - 🥒·LookOutHi') - 2·🪒 + 🥦·(LookInLo' + LookInHi') + 🥒·(LookOutLo' + LookOutHi'))`
+ `+ IsPadding'·(LookupTableClientLogDerivative' - LookupTableClientLogDerivative)` + +## Terminal Constraints + +None. diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index 40a09e126..e0a5ae067 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -319,8 +319,8 @@ pub enum CascadeExtTableColumn { /// The (running sum of the) logarithmic derivative for the Lookup Argument with the Hash Table. /// In every row, the sum accumulates `LookupMultiplicity / (X - Combo)` where `X` is a /// verifier-supplied challenge and `Combo` is the weighted sum of - /// - `LookInHi · 2^16 + LookInLo`, and - /// - `LookOutHi · 2^16 + LookOutLo` + /// - `2^8·LookInHi + LookInLo`, and + /// - `2^8·LookOutHi + LookOutLo` /// with weights supplied by the verifier. HashTableServerLogDerivative, From 2d9e167c33c02a96281eae4ca97d5ecf9b567f59 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 9 Mar 2023 17:10:03 +0100 Subject: [PATCH 45/49] add missing constraints to Lookup Table --- triton-vm/src/table/lookup_table.rs | 30 +++++++++++++++++++++++------ triton-vm/src/table/table_column.rs | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/triton-vm/src/table/lookup_table.rs b/triton-vm/src/table/lookup_table.rs index 1ec9cf55f..a3c8cfce9 100644 --- a/triton-vm/src/table/lookup_table.rs +++ b/triton-vm/src/table/lookup_table.rs @@ -11,10 +11,7 @@ use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; use crate::table::challenges::ChallengeId; -use crate::table::challenges::ChallengeId::CascadeLookupIndeterminate; -use crate::table::challenges::ChallengeId::LookupTableInputWeight; -use crate::table::challenges::ChallengeId::LookupTableOutputWeight; -use crate::table::challenges::ChallengeId::LookupTablePublicIndeterminate; +use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::ConstraintCircuit; use crate::table::constraint_circuit::ConstraintCircuitBuilder; @@ -172,7 +169,17 @@ impl ExtLookupTable { } pub fn ext_consistency_constraints_as_circuits() -> Vec> { - vec![] + let circuit_builder = ConstraintCircuitBuilder::new(); + let constant = |c: u64| circuit_builder.b_constant(c.into()); + let base_row = |col_id: LookupBaseTableColumn| { + circuit_builder.input(BaseRow(col_id.master_base_table_index())) + }; + + let padding_is_0_or_1 = base_row(IsPadding) * (constant(1) - base_row(IsPadding)); + + let mut constraints = [padding_is_0_or_1]; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } pub fn ext_transition_constraints_as_circuits() -> Vec> { @@ -257,6 +264,17 @@ impl ExtLookupTable { } pub fn ext_terminal_constraints_as_circuits() -> Vec> { - vec![] + let circuit_builder = ConstraintCircuitBuilder::new(); + let challenge = |challenge_id: ChallengeId| circuit_builder.challenge(challenge_id); + let ext_row = |col_id: LookupExtTableColumn| { + circuit_builder.input(ExtRow(col_id.master_ext_table_index())) + }; + + let narrow_table_terminal_matches_user_supplied_terminal = + ext_row(PublicEvaluationArgument) - challenge(LookupTablePublicTerminal); + + let mut constraints = [narrow_table_terminal_matches_user_supplied_terminal]; + ConstraintCircuitMonad::constant_folding(&mut constraints); + constraints.map(|circuit| circuit.consume()).to_vec() } } diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index e0a5ae067..2c4a00498 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -360,7 +360,7 @@ pub enum LookupExtTableColumn { CascadeTableServerLogDerivative, /// The running sum for the public evaluation argument of the Lookup Table. - /// In every row, accumulates the verifier-weighted combination of `LookIn` and `LookOut`. + /// In every row, accumulates `LookOut`. PublicEvaluationArgument, } From dd3ccc2a357c9db072fa02c6de28bb9942570818 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 9 Mar 2023 17:32:16 +0100 Subject: [PATCH 46/49] add specification for Lookup Table --- specification/src/lookup-table.md | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/specification/src/lookup-table.md b/specification/src/lookup-table.md index 00b3b2a4a..bc278de4c 100644 --- a/specification/src/lookup-table.md +++ b/specification/src/lookup-table.md @@ -1 +1,88 @@ # Lookup Table + +The Lookup Table helps arithmetizing the lookups necessary for the split-and-lookup S-box used in the Tip5 permutation. +It works in tandem with the [Cascade Table](cascade-table.md). +In the language of the [Tip5 paper](https://eprint.iacr.org/2023/107.pdf), it is a “narrow lookup table.” +This means it is always fully populated, independent of the actual number of lookups. + +Correct creation of the Lookup Table is guaranteed through a public-facing [Evaluation Argument](evaluation-argument.md): +after sampling some challenge $X$, the verifier computes the terminal of the Evaluation Argument over the list of all the expected lookup values with respect to challenge $X$. +The equality of this verifier-supplied terminal against the similarly computed, in-table part of the Evaluation Argument is checked by the Lookup Table's terminal constraint. + +## Base Columns + +The Lookup Table has 4 base columns: + +| name | description | +|:---------------------|:--------------------------------------------| +| `IsPadding` | Indicator for padding rows. | +| `LookIn` | The lookup input. | +| `LookOut` | The lookup output. | +| `LookupMultiplicity` | The number of times the value is looked up. | + +## Extension Columns + +The Lookup Table has 2 extension columns: + +- `CascadeTableServerLogDerivative`, the (running sum of the) logarithmic derivative for the Lookup Argument with the Cascade Table. + In every row, accumulates the summand `LookupMultiplicity / Combo` where `Combo` is the verifier-weighted combination of `LookIn` and `LookOut`. +- `PublicEvaluationArgument`, the running sum for the public evaluation argument of the Lookup Table. + In every row, accumulates `LookOut`. + +## Padding + +Each padding row is the all-zero row with the exception of `IsPadding`, which is 1. + +# Arithmetic Intermediate Representation + +Let all household items (🪥, 🛁, etc.) be challenges, concretely evaluation points, supplied by the verifier. +Let all fruit & vegetables (🥝, 🥥, etc.) be challenges, concretely weights to compress rows, supplied by the verifier. +Both types of challenges are X-field elements, _i.e._, elements of $\mathbb{F}_{p^3}$. + +## Initial Constraints + +1. `LookIn` is 0. +1. `CascadeTableServerLogDerivative` has accumulated the first row with respect to challenges 🍒 and 🍓 and indeterminate 🧺. +1. `PublicEvaluationArgument` has accumulated the first `LookOut` with respect to indeterminate 🧹. + +### Initial Constraints as Polynomials + +1. `LookIn` +1. `CascadeTableServerLogDerivative·(🧺 - 🍒·LookIn - 🍓·LookOut) - LookupMultiplicity` +1. `PublicEvaluationArgument - 🧹 - LookOut` + +## Consistency Constraints + +1. `IsPadding` is 0 or 1. + +### Consistency Constraints as Polynomials + +1. `IsPadding·(1 - IsPadding)` + +## Transition Constraints + +1. If the current row is a padding row, then the next row is a padding row. +1. If the next row is not a padding row, `LookIn` increments by 1. + Else, `LookIn` is 0. +1. If the next row is not a padding row, `CascadeTableServerLogDerivative` accumulates the next row with respect to challenges 🍒 and 🍓 and indeterminate 🧺. + Else, `CascadeTableServerLogDerivative` remains unchanged. +1. If the next row is not a padding row, `PublicEvaluationArgument` accumulates the next `LookOut` with respect to indeterminate 🧹. + Else, `PublicEvaluationArgument` remains unchanged. + +### Transition Constraints as Polynomials + +1. `IsPadding·(1 - IsPadding')` +1. `((1 - IsPadding')·(LookIn' - LookIn - 1))`
+ `+ IsPadding'·LookIn'` +1. `(1 - IsPadding')·((CascadeTableServerLogDerivative' - CascadeTableServerLogDerivative)·(🧺 - 🍒·LookIn' - 🍓·LookOut') - LookupMultiplicity')`
+ `+ IsPadding'·(CascadeTableServerLogDerivative' - CascadeTableServerLogDerivative)` +1. `(1 - IsPadding')·((PublicEvaluationArgument' - PublicEvaluationArgument)·(🧹 - lookup_output'))`
+ `+ IsPadding'·(PublicEvaluationArgument' - PublicEvaluationArgument)` + +## Terminal Constraints + +1. `PublicEvaluationArgument` matches verifier-supplied challenge 🪠. + +### Terminal Constraints as Polynomials + +1. `PublicEvaluationArgument` - 🪠 From 1e05567a3f69ae8237bc7904dd79ed2e53d6548b Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 10 Mar 2023 00:08:17 +0100 Subject: [PATCH 47/49] =?UTF-8?q?add=20new=20tables=20to=20cheatsheet-gene?= =?UTF-8?q?rator=20=E2=80=9Ctests=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- triton-vm/src/stark.rs | 12 ++++++++++++ triton-vm/src/table/master_table.rs | 2 ++ triton-vm/src/table/processor_table.rs | 2 ++ 3 files changed, 16 insertions(+) diff --git a/triton-vm/src/stark.rs b/triton-vm/src/stark.rs index 93ab73f46..092635b74 100644 --- a/triton-vm/src/stark.rs +++ b/triton-vm/src/stark.rs @@ -987,6 +987,7 @@ pub(crate) mod triton_stark_tests { "; let (_, master_base_table, _) = parse_simulate_pad(program, vec![], vec![]); + println!(); println!("Processor Table:"); println!( "| clk | pi | ci | nia | st0 \ @@ -1209,6 +1210,8 @@ pub(crate) mod triton_stark_tests { "ram table", "jump stack table", "hash table", + "cascade table", + "lookup table", "u32 table", "cross-table arg", ]; @@ -1219,6 +1222,8 @@ pub(crate) mod triton_stark_tests { ExtRamTable::num_initial_quotients(), ExtJumpStackTable::num_initial_quotients(), ExtHashTable::num_initial_quotients(), + ExtCascadeTable::num_initial_quotients(), + ExtLookupTable::num_initial_quotients(), ExtU32Table::num_initial_quotients(), GrandCrossTableArg::num_initial_quotients(), ]; @@ -1229,6 +1234,8 @@ pub(crate) mod triton_stark_tests { ExtRamTable::num_consistency_quotients(), ExtJumpStackTable::num_consistency_quotients(), ExtHashTable::num_consistency_quotients(), + ExtCascadeTable::num_consistency_quotients(), + ExtLookupTable::num_consistency_quotients(), ExtU32Table::num_consistency_quotients(), GrandCrossTableArg::num_consistency_quotients(), ]; @@ -1239,6 +1246,8 @@ pub(crate) mod triton_stark_tests { ExtRamTable::num_transition_quotients(), ExtJumpStackTable::num_transition_quotients(), ExtHashTable::num_transition_quotients(), + ExtCascadeTable::num_transition_quotients(), + ExtLookupTable::num_transition_quotients(), ExtU32Table::num_transition_quotients(), GrandCrossTableArg::num_transition_quotients(), ]; @@ -1249,6 +1258,8 @@ pub(crate) mod triton_stark_tests { ExtRamTable::num_terminal_quotients(), ExtJumpStackTable::num_terminal_quotients(), ExtHashTable::num_terminal_quotients(), + ExtCascadeTable::num_terminal_quotients(), + ExtLookupTable::num_terminal_quotients(), ExtU32Table::num_terminal_quotients(), GrandCrossTableArg::num_terminal_quotients(), ]; @@ -1259,6 +1270,7 @@ pub(crate) mod triton_stark_tests { let num_total_term: usize = all_term.iter().sum(); let num_total = num_total_init + num_total_cons + num_total_trans + num_total_term; + println!(); println!("| Table | Init | Cons | Trans | Term | Sum |"); println!("|:---------------------|------:|------:|------:|------:|------:|"); for (name, num_init, num_cons, num_trans, num_term) in diff --git a/triton-vm/src/table/master_table.rs b/triton-vm/src/table/master_table.rs index 12825afeb..99b4d1fc4 100644 --- a/triton-vm/src/table/master_table.rs +++ b/triton-vm/src/table/master_table.rs @@ -1994,6 +1994,7 @@ mod master_table_tests { /// intended use: `cargo t print_all_table_widths -- --nocapture` #[test] fn print_all_table_widths() { + println!(); println!("| table name | #base cols | #ext cols | full width |"); println!("|:-------------------|-----------:|----------:|-----------:|"); println!( @@ -2069,6 +2070,7 @@ mod master_table_tests { /// intended use: `cargo t print_all_master_table_indices -- --nocapture` #[test] fn print_all_master_table_indices() { + println!(); println!("idx | table | base column"); println!("---:|:------------|:-----------"); for column in ProgramBaseTableColumn::iter() { diff --git a/triton-vm/src/table/processor_table.rs b/triton-vm/src/table/processor_table.rs index 6bd31e561..4ded4f00d 100644 --- a/triton-vm/src/table/processor_table.rs +++ b/triton-vm/src/table/processor_table.rs @@ -3170,6 +3170,7 @@ mod constraint_polynomial_tests { let code = "push 2 push -1 add assert halt"; let program = Program::from_code(code).unwrap(); let (aet, _, _) = simulate_no_input(&program); + println!(); for row in aet.processor_trace.rows() { println!("{}", ProcessorTraceRow { row }); } @@ -3715,6 +3716,7 @@ mod constraint_polynomial_tests { (WriteIo, factory.instruction_write_io()), ]; + println!(); println!("| Instruction | #polys | max deg | Degrees"); println!("|:----------------|-------:|--------:|:------------"); for (instruction, constraints) in all_instructions_and_their_transition_constraints { From 9d81b4a74ec94f9e64df170b7352e6cc244e2229 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 10 Mar 2023 00:54:45 +0100 Subject: [PATCH 48/49] update cheatsheet --- specification/cheatsheet.pdf | Bin 121178 -> 117914 bytes specification/cheatsheet.tex | 103 ++++++++++---------------- specification/src/img/triton-logo.pdf | Bin 0 -> 6103 bytes 3 files changed, 38 insertions(+), 65 deletions(-) create mode 100644 specification/src/img/triton-logo.pdf diff --git a/specification/cheatsheet.pdf b/specification/cheatsheet.pdf index f9dedd804a88850201c5e3b3a71907a3a0d9cef7..91fbe0d108b9bcfb419175dd885a7087b7cec8ba 100644 GIT binary patch delta 87616 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�)w> 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 P3>D(XXJu8?Qo{c~j?N-N delta 92398 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%0D+y9@l1`n)!RF<1m9m2;Q~Cb6 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 diff --git a/specification/cheatsheet.tex b/specification/cheatsheet.tex index 0cded8a3d..fd6d32b47 100644 --- a/specification/cheatsheet.tex +++ b/specification/cheatsheet.tex @@ -38,8 +38,16 @@ \begin{document} \pagestyle{empty} +\hfill +\begin{tikzpicture}[overlay] + \node[anchor=north east, inner sep=0] at (1,1.5) { + \includegraphics[keepaspectratio,width=0.4\textwidth]{src/img/triton-logo.pdf} + }; + \node[anchor=north east, inner sep=0] at (1,0) {\texttt{\LARGE v0.18}}; +\end{tikzpicture} + { -\renewcommand{\arraystretch}{0.86} +\renewcommand{\arraystretch}{0.84} \begin{tabular}{rllll} \texttt{ 2} & $\ominus$ & \texttt{pop} & \texttt{\_ st$_0$} & \texttt{\_} \\ \texttt{ 1} & $\oplus$ & \tcbox[colback=instr-arg]{\texttt{push + a}} & \texttt{\_} & \texttt{\_ a} \\ @@ -53,16 +61,16 @@ \texttt{ 32} & $\ovoid$ & \tcbox[colback=instr-jsp]{\texttt{recurse}} & \texttt{\_} & \texttt{\_} \\ \texttt{ 18} & $\ominus$ & \texttt{assert} & \texttt{\_ st$_0$} & \texttt{\_} \\ \texttt{ 0} & $\ovoid$ & \texttt{halt} & \texttt{\_} & \texttt{\_} \\ - \texttt{ 40} & $\ominus$ & \tcbox[colback=instr-mem]{\texttt{read\_mem}} & \texttt{\_ addr st$_0$} & \texttt{\_ addr val} \\ - \texttt{ 26} & $\oplus$ & \tcbox[colback=instr-mem]{\texttt{write\_mem}} & \texttt{\_ addr val} & \texttt{\_ addr val} \\ + \texttt{ 40} & $\ominus$ & \tcbox[colback=instr-mem]{\texttt{read\_mem}} & \texttt{\_ addr} & \texttt{\_ addr a} \\ + \texttt{ 26} & $\oplus$ & \tcbox[colback=instr-mem]{\texttt{write\_mem}} & \texttt{\_ addr a} & \texttt{\_ addr} \\ \texttt{ 48} & $\ovoid^{10}$ & \texttt{hash} & \texttt{\_ st$_9$ $\!\!\dots\!\!$ st$_0$} & \texttt{\_ d$_4$ $\!\!\dots\!\!$ d$_0$ 0 $\!\!\dots\!\!$ 0} \\ \texttt{ 56} & $\ovoid^{11}$ & \texttt{divine\_sibling} & \texttt{\_ idx st$_9$ $\!\!\dots\!\!$ st$_5$ d$_4$ $\!\!\dots\!\!$ d$_0$} & \texttt{\_ idx>>1 r$_4$ $\!\!\dots\!\!$ r$_0$ l$_4$ $\!\!\dots\!\!$ l$_0$} \\ \texttt{ 64} & $\ovoid$ & \texttt{assert\_vector} & \texttt{\_} & \texttt{\_} \\ \texttt{ 72} & $\ovoid$ & \texttt{absorb\_init} & \texttt{\_} & \texttt{\_} \\ \texttt{ 80} & $\ovoid$ & \texttt{absorb} & \texttt{\_} & \texttt{\_} \\ \texttt{ 88} & $\ovoid^{10}$ & \texttt{squeeze} & \texttt{\_ st$_9$ $\dots$ st$_0$} & \texttt{\_ sq$_9$ $\dots$ sq$_0$} \\ - \texttt{ 34} & $\ominus^1$ & \texttt{add} & \texttt{\_ st$_1$ st$_0$} & \texttt{\_ sum} \\ - \texttt{ 42} & $\ominus^1$ & \texttt{mul} & \texttt{\_ st$_1$ st$_0$} & \texttt{\_ prod} \\ + \texttt{ 34} & $\ominus^1$ & \texttt{add} & \texttt{\_ st$_1$ st$_0$} & \texttt{\_ (st$_0$+st$_1$)} \\ + \texttt{ 42} & $\ominus^1$ & \texttt{mul} & \texttt{\_ st$_1$ st$_0$} & \texttt{\_ (st$_0\cdot$st$_1$)} \\ \texttt{ 96} & $\ovoid^1$ & \texttt{invert} & \texttt{\_ st$_0$} & \texttt{\_ st$_0^{-1}$} \\ \texttt{ 50} & $\ominus^1$ & \texttt{eq} & \texttt{\_ st$_1$ st$_0$} & \texttt{\_ (st$_0$==st$_1$)} \\ \texttt{ 4} & $\oplus^2$ & \tcbox[colback=instr-u32]{\texttt{split}} & \texttt{\_ st$_0$} & \texttt{\_ hi lo} \\ @@ -77,71 +85,35 @@ \texttt{120} & $\ovoid^3$ & \texttt{xinvert} & \texttt{\_ x$_2$ x$_1$ x$_0$} & \texttt{\_ y$_2$ y$_1$ y$_0$} \\ \texttt{ 58} & $\ominus^3$ & \texttt{xbmul} & \texttt{\_ x$_2$ x$_1$ x$_0$ b} & \texttt{\_ y$_2$ y$_1$ y$_0$} \\ \texttt{128} & $\oplus$ & \texttt{read\_io} & \texttt{\_} & \texttt{\_ a} \\ - \texttt{ 66} & $\ominus$ & \texttt{write\_io} & \texttt{\_ st$_0$} & \texttt{\_} + \texttt{ 66} & $\ominus$ & \texttt{write\_io} & \texttt{\_ a} & \texttt{\_} \end{tabular} } \newpage -\hspace*{-2.5em}% -\scalebox{0.71}{ - \rowcolors{2}{row2}{row1} -\begin{tabular}{llllllllllllllllllllllll} - \toprule - Table & \multicolumn{5}{l}{Base Columns} & & & & & & & & & & & & & & & & & & \\ \midrule - Program & \multicolumn{3}{l}{Address} & & \multicolumn{3}{l}{Instruction} & \multicolumn{4}{l}{LookupMultiplicity} & \multicolumn{3}{l}{IsPadding} & & & & & & & & & \\ - Processor & \texttt{CLK} & IsPadding & \texttt{IP} & \texttt{PI} & \texttt{CI} & \texttt{NIA} & \texttt{IB0} & \dots & \texttt{IB7} & \texttt{JSP} & \texttt{JSO} & \texttt{JSD} & \texttt{ST0} & \dots & \texttt{ST15} & \texttt{OSP} & \texttt{OSV} & \texttt{HV0} & \dots & \texttt{HV3} & \texttt{RAMP} & \texttt{RAMV} & \texttt{cjd\_mul} \\ - OpStack & \texttt{CLK} & & & & & & & \multicolumn{4}{l}{\texttt{IB1} ($\widehat{=}$ shrink stack)} & & & & & \texttt{OSP} & \texttt{OSV} & & & & & & \\ - RAM & \texttt{CLK} & & & \texttt{PI} & & & \multicolumn{2}{l}{\texttt{bcpc0}} & \multicolumn{2}{l}{\texttt{bcpc1}} & & & & & & & & \multicolumn{3}{l}{\texttt{RAMP}DiffInv} & \texttt{RAMP} & \texttt{RAMV} & \\ - JumpStack & \texttt{CLK} & & & & \texttt{CI} & & & & & \texttt{JSP} & \texttt{JSO} & \texttt{JSD} & & & & & & & & & & & \\ - Hash & \multicolumn{4}{l}{RoundNumber} & \texttt{CI} & & & & & & & & \texttt{ST0} & \dots & \texttt{ST15} & \multicolumn{3}{r}{\texttt{CONSTANT0A}} & \dots & \multicolumn{3}{l}{\texttt{CONSTANT15B}} & \\ - U32 & \texttt{CF} & \multicolumn{3}{l}{\texttt{Bits\quad Bits-33\_inv}} & \texttt{CI} & \texttt{LHS} & \multicolumn{2}{l}{\texttt{LhsInv}} & \texttt{RHS} & \multicolumn{2}{l}{\texttt{RhsInv}} & \multicolumn{2}{l}{\texttt{Result}} & \multicolumn{5}{l}{\texttt{LookupMultiplicity}} & & & & & \\ \bottomrule -\end{tabular} -} %end scalebox -\begin{tikzpicture}[remember picture, overlay] - \node[anchor=south west,inner sep=0] at (4,-11) {\includegraphics[keepaspectratio,width=0.45\textwidth]{src/img/aet-relations.pdf}}; -\end{tikzpicture} -\begin{minipage}[t][0.6\textheight][s]{0.3\textwidth} - \vfill - \rowcolors{2}{row2}{row1} - \begin{tabular}{rl} - \toprule - \#clk & instruction \\ \midrule - 2 & \texttt{neg} \\ - 4 & \texttt{sub} \\ - 7 & \texttt{is\_u32} \\ - 3 & \texttt{lsb} \\ \bottomrule - \end{tabular} - \vspace*{3em} - +\ \\[2cm] +\begin{center} +\includegraphics[keepaspectratio,width=0.7\textwidth]{src/img/aet-relations.pdf} +\end{center} +\ \\[2cm] +\begin{minipage}{0.5\textwidth} \rowcolors{2}{row2}{row1} \begin{tabular}{lrrr} \toprule - & base & ext & $\sum$ \\ \midrule - Program & 4 & 1 & 5 \\ - Processor & 42 & 11 & 53 \\ - OpStack & 4 & 2 & 6 \\ - RAM & 7 & 6 & 13 \\ - JumpStack & 5 & 2 & 7 \\ - Hash & 50 & 3 & 53 \\ - U32 & 10 & 1 & 11 \\ \bottomrule\bottomrule - $\sum$ & 122 & 26 & 148 + & base & ext & $\sum$ \\ \midrule + Program & 4 & 1 & 5 \\ + Processor & 42 & 11 & 53 \\ + OpStack & 4 & 2 & 6 \\ + RAM & 7 & 6 & 13 \\ + JumpStack & 5 & 2 & 7 \\ + Hash & 66 & 19 & 85 \\ + Cascade & 6 & 2 & 8 \\ + Lookup & 4 & 2 & 6 \\ + U32 & 10 & 1 & 11 \\ \bottomrule + $\sum$ & 148 & 46 & 194 \\ + & & & \end{tabular} \end{minipage}% -\hfill% -\begin{minipage}[t][0.613\textheight][b]{0.5\textwidth} - \hfill - \rowcolors{3}{row1}{row2} - \begin{tabular}{lrr} - \multicolumn{3}{l}{$p = 18446744069414584321$} \\ \toprule - $i$ & $\mathbb{F}_p(\nicefrac{1}{i})$ & $-\mathbb{F}_p(\nicefrac{1}{i})$ \\ \midrule - 2 & 092\dots\!161 & 922\dots\!160 \\ - 3 & 122\dots\!881 & 614\dots\!440 \\ - 4 & 138\dots\!241 & 461\dots\!080 \\ - 5 & 147\dots\!457 & 368\dots\!864 \\ - 6 & 153\dots\!601 & 307\dots\!720 \\ \bottomrule - \end{tabular} - \vspace*{3em} - +\begin{minipage}{0.5\textwidth} \hfill \rowcolors{2}{row2}{row1} \begin{tabular}{lrrrrr} @@ -152,11 +124,12 @@ OpStack & 5 & & 4 & & 9 \\ Ram & 8 & & 12 & 1 & 21 \\ JumpStack & 6 & & 6 & & 12 \\ - Hash & 5 & 40 & 26 & & 71 \\ + Hash & 21 & 41 & 42 & & 104 \\ + Cascade & 2 & 1 & 3 & & 6 \\ + Lookup & 3 & 1 & 4 & 1 & 9 \\ U32 & 1 & 14 & 21 & 2 & 38 \\ - Cross-Table & & & & 1 & 1 \\ \bottomrule\bottomrule - $\sum$ & 64 & 66 & 145 & 5 & 280 + Cross-Table & & & & 1 & 1 \\ \bottomrule + $\sum$ & 85 & 69 & 168 & 6 & 328 \end{tabular} \end{minipage} - \end{document} diff --git a/specification/src/img/triton-logo.pdf b/specification/src/img/triton-logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ce311999229d62f1209c9c8dd62b9f4fa07e1806 GIT binary patch literal 6103 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 WR2qRoTaPYA79|T45z#c%g8dg5aM{@a literal 0 HcmV?d00001 From 001a4addcb0baa4a553143a0c0b585d81adc7f98 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 10 Mar 2023 11:57:24 +0100 Subject: [PATCH 49/49] update number of tables in specification --- specification/src/arithmetization.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specification/src/arithmetization.md b/specification/src/arithmetization.md index d7b30a663..9802be49d 100644 --- a/specification/src/arithmetization.md +++ b/specification/src/arithmetization.md @@ -11,7 +11,7 @@ In the nomenclature of this note, a trace is a special kind of table that tracks ## Algebraic Execution Tables -There are 8 Arithmetic Execution Tables in TritonVM. +There are 9 Arithmetic Execution Tables in TritonVM. Their relation is described by below figure. A a blue arrow indicates a [Permutation Argument](permutation-argument.md), a red arrow indicates an [Evaluation Argument](evaluation-argument.md), a purple arrow indicates a [Lookup Argument](lookup-argument.md), and the green arrow is the [Contiguity Argument](contiguity-of-memory-pointer-regions.md). @@ -21,6 +21,7 @@ Public (but not secret!) input and output are given to the Verifier explicitly. As a consequence, neither the input nor the output symbols are recorded in a table. Correct use of public input (respectively, output) in the Processor is ensured through an Evaluation Argument. Given the list of input (or output) symbols, the verifier can compute the Evaluation Argument's terminal explicitly, and consequently compare it to the corresponding terminal in the Processor Table. +The correctness of the [Lookup Table](lookup-table.md) is checked analogously. Despite the fact that neither public input nor output have a dedicated table, them having Evaluation Arguments with the Processor Table justifies their appearance in above figure.