From f295acbe51b62600baeb7cf5e24c39a963f627c8 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 17 Oct 2023 12:30:09 +0200 Subject: [PATCH 01/27] refactor: handle multiple co-processor calls more easily --- triton-vm/src/aet.rs | 14 +-- triton-vm/src/program.rs | 6 +- triton-vm/src/vm.rs | 189 +++++++++++++++++++-------------------- 3 files changed, 100 insertions(+), 109 deletions(-) diff --git a/triton-vm/src/aet.rs b/triton-vm/src/aet.rs index c7453abf0..0be62fae1 100644 --- a/triton-vm/src/aet.rs +++ b/triton-vm/src/aet.rs @@ -204,18 +204,10 @@ impl AlgebraicExecutionTrace { pub fn record_co_processor_call(&mut self, co_processor_call: CoProcessorCall) { match co_processor_call { - Tip5Trace(Instruction::Hash, tip5_trace) => self.append_hash_trace(*tip5_trace), + Tip5Trace(Instruction::Hash, trace) => self.append_hash_trace(*trace), SpongeStateReset => self.append_initial_sponge_state(), - Tip5Trace(instruction, tip5_trace) => { - self.append_sponge_trace(instruction, *tip5_trace) - } - SingleU32TableEntry(u32_entry) => { - self.record_u32_table_entry(u32_entry); - } - DoubleU32TableEntry([u32_entry_0, u32_entry_1]) => { - self.record_u32_table_entry(u32_entry_0); - self.record_u32_table_entry(u32_entry_1); - } + Tip5Trace(instruction, trace) => self.append_sponge_trace(instruction, *trace), + U32Call(u32_entry) => self.record_u32_table_entry(u32_entry), } } diff --git a/triton-vm/src/program.rs b/triton-vm/src/program.rs index 550d214d5..dbcb813f9 100644 --- a/triton-vm/src/program.rs +++ b/triton-vm/src/program.rs @@ -366,9 +366,9 @@ impl Program { false => bail!(InstructionPointerOverflow(state.instruction_pointer)), } - let maybe_co_processor_call = state.step()?; - if let Some(co_processor_call) = maybe_co_processor_call { - aet.record_co_processor_call(co_processor_call); + let co_processor_calls = state.step()?; + for call in co_processor_calls { + aet.record_co_processor_call(call); } } diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 3acebb257..f604b9de4 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -102,8 +102,7 @@ pub enum CoProcessorCall { /// One row per round in the Tip5 permutation. Tip5Trace(Instruction, Box), - SingleU32TableEntry(U32TableEntry), - DoubleU32TableEntry([U32TableEntry; 2]), + U32Call(U32TableEntry), } impl<'pgm> VMState<'pgm> { @@ -206,13 +205,13 @@ impl<'pgm> VMState<'pgm> { } /// Perform the state transition as a mutable operation on `self`. - pub fn step(&mut self) -> Result> { + pub fn step(&mut self) -> Result> { // trying to read past the end of the program doesn't change the previous instruction if let Ok(instruction) = self.current_instruction() { self.previous_instruction = instruction.opcode_b(); } - let maybe_co_processor_trace = match self.current_instruction()? { + let co_processor_calls = match self.current_instruction()? { Pop => self.pop()?, Push(field_element) => self.push(field_element), Divine => self.divine()?, @@ -258,83 +257,83 @@ impl<'pgm> VMState<'pgm> { } self.cycle_count += 1; - Ok(maybe_co_processor_trace) + Ok(co_processor_calls) } - fn pop(&mut self) -> Result> { + fn pop(&mut self) -> Result> { self.op_stack.pop()?; self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn push(&mut self, element: BFieldElement) -> Option { + fn push(&mut self, element: BFieldElement) -> Vec { self.op_stack.push(element); self.instruction_pointer += 2; - None + vec![] } - fn divine(&mut self) -> Result> { + fn divine(&mut self) -> Result> { let element = self.secret_individual_tokens.pop_front().ok_or(anyhow!( "Instruction `divine`: secret input buffer is empty." ))?; self.op_stack.push(element); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn dup(&mut self, stack_register: OpStackElement) -> Option { + fn dup(&mut self, stack_register: OpStackElement) -> Vec { let element = self.op_stack.peek_at(stack_register); self.op_stack.push(element); self.instruction_pointer += 2; - None + vec![] } - fn swap(&mut self, stack_register: OpStackElement) -> Result> { + fn swap(&mut self, stack_register: OpStackElement) -> Result> { if stack_register == ST0 { bail!(SwapST0); } self.op_stack.swap_top_with(stack_register); self.instruction_pointer += 2; - Ok(None) + Ok(vec![]) } - fn nop(&mut self) -> Option { + fn nop(&mut self) -> Vec { self.instruction_pointer += 1; - None + vec![] } - fn skiz(&mut self) -> Result> { + fn skiz(&mut self) -> Result> { let top_of_stack = self.op_stack.pop()?; self.instruction_pointer += match top_of_stack.is_zero() { true => 1 + self.next_instruction()?.size(), false => 1, }; - Ok(None) + Ok(vec![]) } - fn call(&mut self, call_destination: BFieldElement) -> Option { + fn call(&mut self, call_destination: BFieldElement) -> Vec { let size_of_instruction_call = 2; let call_origin = (self.instruction_pointer as u32 + size_of_instruction_call).into(); let jump_stack_entry = (call_origin, call_destination); self.jump_stack.push(jump_stack_entry); self.instruction_pointer = call_destination.value() as usize; - None + vec![] } - fn return_from_call(&mut self) -> Result> { + fn return_from_call(&mut self) -> Result> { let (call_origin, _) = self.jump_stack_pop()?; self.instruction_pointer = call_origin.value() as usize; - Ok(None) + Ok(vec![]) } - fn recurse(&mut self) -> Result> { + fn recurse(&mut self) -> Result> { let (_, call_destination) = self.jump_stack_peek()?; self.instruction_pointer = call_destination.value() as usize; - Ok(None) + Ok(vec![]) } - fn assert(&mut self) -> Result> { + fn assert(&mut self) -> Result> { let top_of_stack = self.op_stack.pop()?; if !top_of_stack.is_one() { let assertion_failed = @@ -342,34 +341,34 @@ impl<'pgm> VMState<'pgm> { bail!(assertion_failed); } self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn halt(&mut self) -> Option { + fn halt(&mut self) -> Vec { self.halting = true; self.instruction_pointer += 1; - None + vec![] } - fn read_mem(&mut self) -> Option { + fn read_mem(&mut self) -> Vec { let ram_pointer = self.op_stack.peek_at(ST0); let ram_value = self.memory_get(&ram_pointer); self.op_stack.push(ram_value); self.ramp = ram_pointer.value(); self.instruction_pointer += 1; - None + vec![] } - fn write_mem(&mut self) -> Result> { + fn write_mem(&mut self) -> Result> { let ram_pointer = self.op_stack.peek_at(ST1); let ram_value = self.op_stack.pop()?; self.ramp = ram_pointer.value(); self.ram.insert(ram_pointer, ram_value); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn hash(&mut self) -> Result> { + fn hash(&mut self) -> Result> { let to_hash = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; let mut hash_input = Tip5State::new(Domain::FixedLength); hash_input.state[..tip5::RATE].copy_from_slice(&to_hash); @@ -382,19 +381,19 @@ impl<'pgm> VMState<'pgm> { for _ in 0..DIGEST_LENGTH { self.op_stack.push(BFieldElement::zero()); } - let co_processor_trace = Some(Tip5Trace(Hash, Box::new(tip5_trace))); + let co_processor_calls = vec![Tip5Trace(Hash, Box::new(tip5_trace))]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn sponge_init(&mut self) -> Option { + fn sponge_init(&mut self) -> Vec { self.sponge_state = Tip5::init().state; self.instruction_pointer += 1; - Some(SpongeStateReset) + vec![SpongeStateReset] } - fn sponge_absorb(&mut self) -> Result> { + fn sponge_absorb(&mut self) -> Result> { // fetch top elements but don't alter the stack let to_absorb = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; for i in (0..tip5::RATE).rev() { @@ -406,13 +405,13 @@ impl<'pgm> VMState<'pgm> { state: self.sponge_state, }); self.sponge_state = tip5_trace.last().unwrap().to_owned(); - let co_processor_trace = Some(Tip5Trace(SpongeAbsorb, Box::new(tip5_trace))); + let co_processor_calls = vec![Tip5Trace(SpongeAbsorb, Box::new(tip5_trace))]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn sponge_squeeze(&mut self) -> Result> { + fn sponge_squeeze(&mut self) -> Result> { let _ = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; for i in (0..tip5::RATE).rev() { self.op_stack.push(self.sponge_state[i]); @@ -421,13 +420,13 @@ impl<'pgm> VMState<'pgm> { state: self.sponge_state, }); self.sponge_state = tip5_trace.last().unwrap().to_owned(); - let co_processor_trace = Some(Tip5Trace(SpongeSqueeze, Box::new(tip5_trace))); + let co_processor_calls = vec![Tip5Trace(SpongeSqueeze, Box::new(tip5_trace))]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn divine_sibling(&mut self) -> Result> { + fn divine_sibling(&mut self) -> Result> { let _st0_through_st4 = self.op_stack.pop_multiple::<{ DIGEST_LENGTH }>()?; let known_digest = self.op_stack.pop_multiple()?; @@ -446,10 +445,10 @@ impl<'pgm> VMState<'pgm> { self.op_stack.push(digest_element); } self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn assert_vector(&mut self) -> Result> { + fn assert_vector(&mut self) -> Result> { for index in 0..DIGEST_LENGTH { let lhs = index.try_into().unwrap(); let rhs = (index + DIGEST_LENGTH).try_into().unwrap(); @@ -458,7 +457,7 @@ impl<'pgm> VMState<'pgm> { } } self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } fn new_vector_assertion_failure(&self, failing_index_lhs: usize) -> InstructionError { @@ -473,42 +472,42 @@ impl<'pgm> VMState<'pgm> { ) } - fn add(&mut self) -> Result> { + fn add(&mut self) -> Result> { let lhs = self.op_stack.pop()?; let rhs = self.op_stack.pop()?; self.op_stack.push(lhs + rhs); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn mul(&mut self) -> Result> { + fn mul(&mut self) -> Result> { let lhs = self.op_stack.pop()?; let rhs = self.op_stack.pop()?; self.op_stack.push(lhs * rhs); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn invert(&mut self) -> Result> { + fn invert(&mut self) -> Result> { let top_of_stack = self.op_stack.pop()?; if top_of_stack.is_zero() { bail!(InverseOfZero); } self.op_stack.push(top_of_stack.inverse()); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn eq(&mut self) -> Result> { + fn eq(&mut self) -> Result> { let lhs = self.op_stack.pop()?; let rhs = self.op_stack.pop()?; let eq: u32 = (lhs == rhs).into(); self.op_stack.push(eq.into()); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn split(&mut self) -> Result> { + fn split(&mut self) -> Result> { let top_of_stack = self.op_stack.pop()?; let lo = BFieldElement::new(top_of_stack.value() & 0xffff_ffff); let hi = BFieldElement::new(top_of_stack.value() >> 32); @@ -516,39 +515,39 @@ impl<'pgm> VMState<'pgm> { self.op_stack.push(lo); let u32_table_entry = U32TableEntry::new_from_base_field_element(Split, lo, hi); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn lt(&mut self) -> Result> { + fn lt(&mut self) -> Result> { let lhs = self.op_stack.pop_u32()?; let rhs = self.op_stack.pop_u32()?; let lt: u32 = (lhs < rhs).into(); self.op_stack.push(lt.into()); let u32_table_entry = U32TableEntry::new(Lt, lhs, rhs); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn and(&mut self) -> Result> { + fn and(&mut self) -> Result> { let lhs = self.op_stack.pop_u32()?; let rhs = self.op_stack.pop_u32()?; let and = lhs & rhs; self.op_stack.push(and.into()); let u32_table_entry = U32TableEntry::new(And, lhs, rhs); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn xor(&mut self) -> Result> { + fn xor(&mut self) -> Result> { let lhs = self.op_stack.pop_u32()?; let rhs = self.op_stack.pop_u32()?; let xor = lhs ^ rhs; @@ -558,13 +557,13 @@ impl<'pgm> VMState<'pgm> { // and `xor` instruction using the u32 coprocessor's `and` capability: // a ^ b = a + b - 2 · (a & b) let u32_table_entry = U32TableEntry::new(And, lhs, rhs); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn log_2_floor(&mut self) -> Result> { + fn log_2_floor(&mut self) -> Result> { let top_of_stack = self.op_stack.pop_u32()?; if top_of_stack.is_zero() { bail!(LogarithmOfZero); @@ -573,13 +572,13 @@ impl<'pgm> VMState<'pgm> { self.op_stack.push(log_2_floor.into()); let u32_table_entry = U32TableEntry::new(Log2Floor, top_of_stack, 0); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn pow(&mut self) -> Result> { + fn pow(&mut self) -> Result> { let base = self.op_stack.pop()?; let exponent = self.op_stack.pop_u32()?; let base_pow_exponent = base.mod_pow(exponent.into()); @@ -587,13 +586,13 @@ impl<'pgm> VMState<'pgm> { let u32_table_entry = U32TableEntry::new_from_base_field_element(Pow, base, exponent.into()); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn div_mod(&mut self) -> Result> { + fn div_mod(&mut self) -> Result> { let numerator = self.op_stack.pop_u32()?; let denominator = self.op_stack.pop_u32()?; if denominator.is_zero() { @@ -607,44 +606,44 @@ impl<'pgm> VMState<'pgm> { let remainder_is_less_than_denominator = U32TableEntry::new(Lt, remainder, denominator); let numerator_and_quotient_range_check = U32TableEntry::new(Split, numerator, quotient); - let co_processor_trace = Some(DoubleU32TableEntry([ - remainder_is_less_than_denominator, - numerator_and_quotient_range_check, - ])); + let co_processor_calls = vec![ + U32Call(remainder_is_less_than_denominator), + U32Call(numerator_and_quotient_range_check), + ]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn pop_count(&mut self) -> Result> { + fn pop_count(&mut self) -> Result> { let top_of_stack = self.op_stack.pop_u32()?; let pop_count = top_of_stack.count_ones(); self.op_stack.push(pop_count.into()); let u32_table_entry = U32TableEntry::new(PopCount, top_of_stack, 0); - let co_processor_trace = Some(SingleU32TableEntry(u32_table_entry)); + let co_processor_calls = vec![U32Call(u32_table_entry)]; self.instruction_pointer += 1; - Ok(co_processor_trace) + Ok(co_processor_calls) } - fn xx_add(&mut self) -> Result> { + fn xx_add(&mut self) -> Result> { let lhs = self.op_stack.pop_extension_field_element()?; let rhs = self.op_stack.peek_at_top_extension_field_element(); self.op_stack.push_extension_field_element(lhs + rhs); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn xx_mul(&mut self) -> Result> { + fn xx_mul(&mut self) -> Result> { let lhs = self.op_stack.pop_extension_field_element()?; let rhs = self.op_stack.peek_at_top_extension_field_element(); self.op_stack.push_extension_field_element(lhs * rhs); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn x_invert(&mut self) -> Result> { + fn x_invert(&mut self) -> Result> { let top_of_stack = self.op_stack.pop_extension_field_element()?; if top_of_stack.is_zero() { bail!(InverseOfZero); @@ -652,31 +651,31 @@ impl<'pgm> VMState<'pgm> { self.op_stack .push_extension_field_element(top_of_stack.inverse()); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn xb_mul(&mut self) -> Result> { + fn xb_mul(&mut self) -> Result> { let lhs = self.op_stack.pop()?; let rhs = self.op_stack.pop_extension_field_element()?; self.op_stack.push_extension_field_element(lhs.lift() * rhs); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn write_io(&mut self) -> Result> { + fn write_io(&mut self) -> Result> { let top_of_stack = self.op_stack.pop()?; self.public_output.push(top_of_stack); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } - fn read_io(&mut self) -> Result> { + fn read_io(&mut self) -> Result> { let read_element = self.public_input.pop_front().ok_or(anyhow!( "Instruction `read_io`: public input buffer is empty." ))?; self.op_stack.push(read_element); self.instruction_pointer += 1; - Ok(None) + Ok(vec![]) } pub fn to_processor_row(&self) -> Array1 { From c8eaf8c1ed6cb30af911b3e827c83d4db8d4c0a1 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 17 Oct 2023 12:47:59 +0200 Subject: [PATCH 02/27] refactor: delegate instruction lookup recording to `AET` --- triton-vm/src/aet.rs | 15 +++++++++++++++ triton-vm/src/program.rs | 7 ------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/triton-vm/src/aet.rs b/triton-vm/src/aet.rs index 0be62fae1..0b20c9b06 100644 --- a/triton-vm/src/aet.rs +++ b/triton-vm/src/aet.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::ops::AddAssign; use anyhow::anyhow; +use anyhow::bail; use anyhow::Result; use itertools::Itertools; use ndarray::s; @@ -18,6 +19,7 @@ use twenty_first::shared_math::tip5; use twenty_first::shared_math::tip5::Tip5; use twenty_first::util_types::algebraic_hasher::SpongeHasher; +use crate::error::InstructionError::InstructionPointerOverflow; use crate::instruction::Instruction; use crate::program::Program; use crate::stark::StarkHasher; @@ -197,6 +199,19 @@ impl AlgebraicExecutionTrace { } pub fn record_state(&mut self, state: &VMState) -> Result<()> { + self.record_instruction_lookup(state.instruction_pointer)?; + self.append_state_to_processor_trace(state) + } + + fn record_instruction_lookup(&mut self, instruction_pointer: usize) -> Result<()> { + if instruction_pointer >= self.instruction_multiplicities.len() { + bail!(InstructionPointerOverflow(instruction_pointer)); + } + self.instruction_multiplicities[instruction_pointer] += 1; + Ok(()) + } + + fn append_state_to_processor_trace(&mut self, state: &VMState) -> Result<()> { self.processor_trace .push_row(state.to_processor_row().view()) .map_err(|e| anyhow!(e)) diff --git a/triton-vm/src/program.rs b/triton-vm/src/program.rs index dbcb813f9..8461c9d49 100644 --- a/triton-vm/src/program.rs +++ b/triton-vm/src/program.rs @@ -21,7 +21,6 @@ use twenty_first::util_types::algebraic_hasher::AlgebraicHasher; use crate::aet::AlgebraicExecutionTrace; use crate::ensure_eq; -use crate::error::InstructionError::InstructionPointerOverflow; use crate::instruction::Instruction; use crate::instruction::LabelledInstruction; use crate::parser::parse; @@ -360,12 +359,6 @@ impl Program { while !state.halting { aet.record_state(&state)?; - - match state.instruction_pointer < aet.instruction_multiplicities.len() { - true => aet.instruction_multiplicities[state.instruction_pointer] += 1, - false => bail!(InstructionPointerOverflow(state.instruction_pointer)), - } - let co_processor_calls = state.step()?; for call in co_processor_calls { aet.record_co_processor_call(call); From 546167eb05a02552ca2c162af151e3e4bdc62785 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 17 Oct 2023 22:35:49 +0200 Subject: [PATCH 03/27] doc: remove redundant comments and doc-comments --- triton-vm/src/op_stack.rs | 15 ++------------- triton-vm/src/vm.rs | 3 --- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 8ef04c31c..99dc77b29 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -46,31 +46,26 @@ impl OpStack { Self { stack } } - /// Push an element onto the op-stack. pub(crate) fn push(&mut self, element: BFieldElement) { self.stack.push(element); } - /// Push an extension field element onto the op-stack. pub(crate) fn push_extension_field_element(&mut self, element: XFieldElement) { for coefficient in element.coefficients.into_iter().rev() { self.push(coefficient); } } - /// Pop an element from the op-stack. pub(crate) fn pop(&mut self) -> Result { self.stack.pop().ok_or_else(|| anyhow!(OpStackTooShallow)) } - /// Pop an extension field element from the op-stack. pub(crate) fn pop_extension_field_element(&mut self) -> Result { let coefficients = self.pop_multiple()?; let element = XFieldElement::new(coefficients); Ok(element) } - /// Pop a u32 from the op-stack. pub(crate) fn pop_u32(&mut self) -> Result { let element = self.pop()?; element @@ -78,7 +73,6 @@ impl OpStack { .map_err(|_| anyhow!(FailedU32Conversion(element))) } - /// Pop multiple elements from the op-stack. pub(crate) fn pop_multiple(&mut self) -> Result<[BFieldElement; N]> { let mut popped_elements = [BFieldElement::zero(); N]; for element in popped_elements.iter_mut() { @@ -87,20 +81,17 @@ impl OpStack { Ok(popped_elements) } - /// Fetches the indicated stack element without modifying the stack. pub(crate) fn peek_at(&self, stack_element: OpStackElement) -> BFieldElement { let stack_element_index = usize::from(stack_element); let top_of_stack_index = self.stack.len() - 1; self.stack[top_of_stack_index - stack_element_index] } - /// Fetches the top-most extension field element without modifying the stack. pub(crate) fn peek_at_top_extension_field_element(&self) -> XFieldElement { let coefficients = [self.peek_at(ST0), self.peek_at(ST1), self.peek_at(ST2)]; XFieldElement::new(coefficients) } - /// Swaps the top of the stack with the indicated stack element. pub(crate) fn swap_top_with(&mut self, stack_element: OpStackElement) { let stack_element_index = usize::from(stack_element); let top_of_stack_index = self.stack.len() - 1; @@ -108,14 +99,12 @@ impl OpStack { .swap(top_of_stack_index, top_of_stack_index - stack_element_index); } - /// `true` if and only if the op-stack contains fewer elements than the number of - /// op-stack registers, _i.e._, [`OpStackElement::COUNT`]. pub(crate) fn is_too_shallow(&self) -> bool { self.stack.len() < OpStackElement::COUNT } - /// The address of the next free address of the op-stack. - /// Equivalent to the current length of the op-stack. + /// The address of the next free address of the op-stack. Equivalent to the current length of + /// the op-stack. pub(crate) fn op_stack_pointer(&self) -> BFieldElement { (self.stack.len() as u64).into() } diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index f604b9de4..8f70ba501 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -40,7 +40,6 @@ pub const NUM_HELPER_VARIABLE_REGISTERS: usize = 7; #[derive(Debug, Clone)] pub struct VMState<'pgm> { - // Memory /// The **program memory** stores the instructions (and their arguments) of the program /// currently being executed by Triton VM. It is read-only. pub program: &'pgm [Instruction], @@ -66,7 +65,6 @@ pub struct VMState<'pgm> { /// The **Jump-stack memory** stores the entire jump stack. pub jump_stack: Vec<(BFieldElement, BFieldElement)>, - // Registers /// Number of cycles the program has been running for pub cycle_count: u32, @@ -86,7 +84,6 @@ pub struct VMState<'pgm> { /// exposed outside of the VM. pub sponge_state: [BFieldElement; tip5::STATE_SIZE], - // Bookkeeping /// Indicates whether the terminating instruction `halt` has been executed. pub halting: bool, } From 612714d0045b17dc21f72a37a0e5ae5e7841b304 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 17 Oct 2023 22:37:17 +0200 Subject: [PATCH 04/27] refactor(vm): rename `ramp` to `ram_pointer` --- triton-vm/src/vm.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 8f70ba501..47932dc4d 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -75,7 +75,7 @@ pub struct VMState<'pgm> { pub previous_instruction: BFieldElement, /// RAM pointer - pub ramp: u64, + pub ram_pointer: u64, /// The current state of the one, global Sponge that can be manipulated using instructions /// `SpongeInit`, `SpongeAbsorb`, and `SpongeSqueeze`. Instruction `SpongeInit` resets the @@ -127,7 +127,7 @@ impl<'pgm> VMState<'pgm> { cycle_count: 0, instruction_pointer: 0, previous_instruction: Default::default(), - ramp: 0, + ram_pointer: 0, sponge_state: Default::default(), halting: false, } @@ -351,7 +351,7 @@ impl<'pgm> VMState<'pgm> { let ram_pointer = self.op_stack.peek_at(ST0); let ram_value = self.memory_get(&ram_pointer); self.op_stack.push(ram_value); - self.ramp = ram_pointer.value(); + self.ram_pointer = ram_pointer.value(); self.instruction_pointer += 1; vec![] } @@ -359,7 +359,7 @@ impl<'pgm> VMState<'pgm> { fn write_mem(&mut self) -> Result> { let ram_pointer = self.op_stack.peek_at(ST1); let ram_value = self.op_stack.pop()?; - self.ramp = ram_pointer.value(); + self.ram_pointer = ram_pointer.value(); self.ram.insert(ram_pointer, ram_value); self.instruction_pointer += 1; Ok(vec![]) @@ -682,7 +682,7 @@ impl<'pgm> VMState<'pgm> { let current_instruction = self.current_instruction().unwrap_or(Nop); let helper_variables = self.derive_helper_variables(); - let ram_pointer = self.ramp.into(); + let ram_pointer = self.ram_pointer.into(); processor_row[CLK.base_table_index()] = (self.cycle_count as u64).into(); processor_row[PreviousInstruction.base_table_index()] = self.previous_instruction; From 744086fe919038fbb3dfff94ea4b77c8c3023ee8 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 18 Oct 2023 13:45:06 +0200 Subject: [PATCH 05/27] refactor(opstack): simpler type conversion from `OpStackElement` --- triton-vm/src/op_stack.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 99dc77b29..3f64acfbb 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -1,9 +1,9 @@ use std::fmt::Display; use std::fmt::Formatter; use std::fmt::Result as FmtResult; -use std::result; use anyhow::anyhow; +use anyhow::bail; use anyhow::Result; use get_size::GetSize; use num_traits::Zero; @@ -182,9 +182,9 @@ impl From<&OpStackElement> for u32 { } impl TryFrom for OpStackElement { - type Error = String; + type Error = anyhow::Error; - fn try_from(stack_index: u32) -> result::Result { + fn try_from(stack_index: u32) -> Result { match stack_index { 0 => Ok(ST0), 1 => Ok(ST1), @@ -202,9 +202,7 @@ impl TryFrom for OpStackElement { 13 => Ok(ST13), 14 => Ok(ST14), 15 => Ok(ST15), - _ => Err(format!( - "Index {stack_index} is out of range for `OpStackElement`." - )), + _ => bail!("Index {stack_index} is out of range for `OpStackElement`."), } } } @@ -216,12 +214,10 @@ impl From for u64 { } impl TryFrom for OpStackElement { - type Error = String; + type Error = anyhow::Error; - fn try_from(stack_index: u64) -> result::Result { - let stack_index = u32::try_from(stack_index) - .map_err(|_| format!("Index {stack_index} is out of range for `OpStackElement`."))?; - stack_index.try_into() + fn try_from(stack_index: u64) -> Result { + u32::try_from(stack_index)?.try_into() } } @@ -238,12 +234,10 @@ impl From<&OpStackElement> for usize { } impl TryFrom for OpStackElement { - type Error = String; + type Error = anyhow::Error; - fn try_from(stack_index: usize) -> result::Result { - let stack_index = - u32::try_from(stack_index).map_err(|_| "Cannot convert usize to u32.".to_string())?; - stack_index.try_into() + fn try_from(stack_index: usize) -> Result { + u32::try_from(stack_index)?.try_into() } } From c6e54d44cc4b832df9838dcf21c7ea8b7970a8f7 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 18 Oct 2023 15:52:13 +0200 Subject: [PATCH 06/27] feat: add enum to record changes to op stack underflow --- triton-vm/src/op_stack.rs | 83 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 6 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 3f64acfbb..e49e3b9bc 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -6,6 +6,7 @@ use anyhow::anyhow; use anyhow::bail; use anyhow::Result; use get_size::GetSize; +use itertools::Itertools; use num_traits::Zero; use serde_derive::Deserialize; use serde_derive::Serialize; @@ -113,11 +114,45 @@ impl OpStack { /// is empty. pub(crate) fn op_stack_value(&self) -> BFieldElement { let top_of_stack_index = self.stack.len() - 1; - if top_of_stack_index < OpStackElement::COUNT { - return BFieldElement::zero(); + let underflow_start = top_of_stack_index - OpStackElement::COUNT; + self.stack.get(underflow_start).copied().unwrap_or_default() + } +} + +/// Indicates changes to the op-stack underflow memory. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize)] +pub(crate) enum UnderflowIO { + Read(BFieldElement), + Write(BFieldElement), +} + +impl UnderflowIO { + /// Remove spurious read/write sequences arising from temporary stack changes. + /// + /// For example, the sequence `[Read(5), Write(5), Read(7)]` can be replaced with `[Read(7)]`. + /// Similarly, the sequence `[Write(5), Write(3), Read(3), Read(5), Write(7)]` can be replaced + /// with `[Write(7)]`. + pub(crate) fn canonicalize_sequence(sequence: &mut Vec) { + while let Some(index) = Self::index_of_dual_pair(sequence) { + sequence.remove(index); + sequence.remove(index); + } + } + + fn index_of_dual_pair(sequence: &[Self]) -> Option { + sequence + .iter() + .tuple_windows() + .find_position(|(&left, &right)| left.is_dual_to(right)) + .map(|(index, _)| index) + } + + fn is_dual_to(&self, other: Self) -> bool { + match (self, other) { + (&Self::Read(read), Self::Write(write)) => read == write, + (&Self::Write(write), Self::Read(read)) => read == write, + _ => false, } - let op_stack_value_index = top_of_stack_index - OpStackElement::COUNT; - self.stack[op_stack_value_index] } } @@ -257,8 +292,7 @@ impl From<&OpStackElement> for BFieldElement { mod tests { use twenty_first::shared_math::b_field_element::BFieldElement; - use crate::op_stack::OpStack; - use crate::op_stack::OpStackElement; + use super::*; #[test] fn sanity() { @@ -337,4 +371,41 @@ mod tests { op_stack.pop().expect("can't pop"); assert!(op_stack.is_too_shallow()); } + + #[test] + fn canonicalize_empty_underflow_io_sequence() { + let mut sequence = vec![]; + UnderflowIO::canonicalize_sequence(&mut sequence); + + let expected_sequence = Vec::::new(); + assert_eq!(expected_sequence, sequence); + } + + #[test] + fn canonicalize_simple_underflow_io_sequence() { + let mut sequence = vec![ + UnderflowIO::Read(5_u64.into()), + UnderflowIO::Write(5_u64.into()), + UnderflowIO::Read(7_u64.into()), + ]; + UnderflowIO::canonicalize_sequence(&mut sequence); + + let expected_sequence = vec![UnderflowIO::Read(7_u64.into())]; + assert_eq!(expected_sequence, sequence); + } + + #[test] + fn canonicalize_medium_complex_underflow_io_sequence() { + let mut sequence = vec![ + UnderflowIO::Write(5_u64.into()), + UnderflowIO::Write(3_u64.into()), + UnderflowIO::Read(3_u64.into()), + UnderflowIO::Read(5_u64.into()), + UnderflowIO::Write(7_u64.into()), + ]; + UnderflowIO::canonicalize_sequence(&mut sequence); + + let expected_sequence = vec![UnderflowIO::Write(7_u64.into())]; + assert_eq!(expected_sequence, sequence); + } } From 4323b202cf3cf028d80c91b336283c25a888ad2a Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 19 Oct 2023 10:28:15 +0200 Subject: [PATCH 07/27] chore(test): remove unnecessary paths --- triton-vm/src/op_stack.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index e49e3b9bc..12572681c 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -320,22 +320,22 @@ mod tests { // verify that all accessible items are different let mut container = vec![ - op_stack.peek_at(OpStackElement::ST0), - op_stack.peek_at(OpStackElement::ST1), - op_stack.peek_at(OpStackElement::ST2), - op_stack.peek_at(OpStackElement::ST3), - op_stack.peek_at(OpStackElement::ST4), - op_stack.peek_at(OpStackElement::ST5), - op_stack.peek_at(OpStackElement::ST6), - op_stack.peek_at(OpStackElement::ST7), - op_stack.peek_at(OpStackElement::ST8), - op_stack.peek_at(OpStackElement::ST9), - op_stack.peek_at(OpStackElement::ST10), - op_stack.peek_at(OpStackElement::ST11), - op_stack.peek_at(OpStackElement::ST12), - op_stack.peek_at(OpStackElement::ST13), - op_stack.peek_at(OpStackElement::ST14), - op_stack.peek_at(OpStackElement::ST15), + op_stack.peek_at(ST0), + op_stack.peek_at(ST1), + op_stack.peek_at(ST2), + op_stack.peek_at(ST3), + op_stack.peek_at(ST4), + op_stack.peek_at(ST5), + op_stack.peek_at(ST6), + op_stack.peek_at(ST7), + op_stack.peek_at(ST8), + op_stack.peek_at(ST9), + op_stack.peek_at(ST10), + op_stack.peek_at(ST11), + op_stack.peek_at(ST12), + op_stack.peek_at(ST13), + op_stack.peek_at(ST14), + op_stack.peek_at(ST15), op_stack.op_stack_value(), ]; let len_before = container.len(); From 49424a6ace7945513e17cd7f8d39c884a3e3fb26 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 19 Oct 2023 10:53:23 +0200 Subject: [PATCH 08/27] feat: propagate opstack underflow read/write to VM --- triton-vm/src/op_stack.rs | 58 ++++++++++++++++++++++----------- triton-vm/src/vm.rs | 68 +++++++++++++++++++-------------------- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 12572681c..6315b6ec6 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -15,6 +15,7 @@ use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::digest::Digest; use twenty_first::shared_math::tip5::DIGEST_LENGTH; use twenty_first::shared_math::x_field_element::XFieldElement; +use twenty_first::shared_math::x_field_element::EXTENSION_DEGREE; use crate::error::InstructionError::*; use crate::op_stack::OpStackElement::*; @@ -47,39 +48,58 @@ impl OpStack { Self { stack } } - pub(crate) fn push(&mut self, element: BFieldElement) { + pub(crate) fn push(&mut self, element: BFieldElement) -> UnderflowIO { self.stack.push(element); + UnderflowIO::Write(self.first_underflow_element()) } - pub(crate) fn push_extension_field_element(&mut self, element: XFieldElement) { + pub(crate) fn push_extension_field_element( + &mut self, + element: XFieldElement, + ) -> [UnderflowIO; EXTENSION_DEGREE] { + let mut underflow_io = vec![]; for coefficient in element.coefficients.into_iter().rev() { - self.push(coefficient); + let underflow_write = self.push(coefficient); + underflow_io.push(underflow_write); } + underflow_io.try_into().unwrap() } - pub(crate) fn pop(&mut self) -> Result { - self.stack.pop().ok_or_else(|| anyhow!(OpStackTooShallow)) + pub(crate) fn pop(&mut self) -> Result<(BFieldElement, UnderflowIO)> { + let underflow_io = UnderflowIO::Read(self.first_underflow_element()); + let element = self.stack.pop().ok_or_else(|| anyhow!(OpStackTooShallow))?; + Ok((element, underflow_io)) } - pub(crate) fn pop_extension_field_element(&mut self) -> Result { - let coefficients = self.pop_multiple()?; + pub(crate) fn pop_extension_field_element( + &mut self, + ) -> Result<(XFieldElement, [UnderflowIO; EXTENSION_DEGREE])> { + let (coefficients, underflow_io) = self.pop_multiple()?; let element = XFieldElement::new(coefficients); - Ok(element) + Ok((element, underflow_io)) } - pub(crate) fn pop_u32(&mut self) -> Result { - let element = self.pop()?; - element + pub(crate) fn pop_u32(&mut self) -> Result<(u32, UnderflowIO)> { + let (element, underflow_io) = self.pop()?; + let element = element .try_into() - .map_err(|_| anyhow!(FailedU32Conversion(element))) + .map_err(|_| anyhow!(FailedU32Conversion(element)))?; + Ok((element, underflow_io)) } - pub(crate) fn pop_multiple(&mut self) -> Result<[BFieldElement; N]> { - let mut popped_elements = [BFieldElement::zero(); N]; - for element in popped_elements.iter_mut() { - *element = self.pop()?; + pub(crate) fn pop_multiple( + &mut self, + ) -> Result<([BFieldElement; N], [UnderflowIO; N])> { + let mut elements = vec![]; + let mut underflow_io = vec![]; + for _ in 0..N { + let (element, underflow_read) = self.pop()?; + elements.push(element); + underflow_io.push(underflow_read); } - Ok(popped_elements) + let elements = elements.try_into().unwrap(); + let underflow_io = underflow_io.try_into().unwrap(); + Ok((elements, underflow_io)) } pub(crate) fn peek_at(&self, stack_element: OpStackElement) -> BFieldElement { @@ -112,7 +132,7 @@ impl OpStack { /// The first element of the op-stack underflow memory, or 0 if the op-stack underflow memory /// is empty. - pub(crate) fn op_stack_value(&self) -> BFieldElement { + pub(crate) fn first_underflow_element(&self) -> BFieldElement { let top_of_stack_index = self.stack.len() - 1; let underflow_start = top_of_stack_index - OpStackElement::COUNT; self.stack.get(underflow_start).copied().unwrap_or_default() @@ -336,7 +356,7 @@ mod tests { op_stack.peek_at(ST13), op_stack.peek_at(ST14), op_stack.peek_at(ST15), - op_stack.op_stack_value(), + op_stack.first_underflow_element(), ]; let len_before = container.len(); container.sort_by_key(|a| a.value()); diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 47932dc4d..4b728e5e8 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -300,7 +300,7 @@ impl<'pgm> VMState<'pgm> { } fn skiz(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop()?; + let (top_of_stack, _) = self.op_stack.pop()?; self.instruction_pointer += match top_of_stack.is_zero() { true => 1 + self.next_instruction()?.size(), false => 1, @@ -331,7 +331,7 @@ impl<'pgm> VMState<'pgm> { } fn assert(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop()?; + let (top_of_stack, _) = self.op_stack.pop()?; if !top_of_stack.is_one() { let assertion_failed = AssertionFailed(self.instruction_pointer, self.cycle_count, top_of_stack); @@ -358,7 +358,7 @@ impl<'pgm> VMState<'pgm> { fn write_mem(&mut self) -> Result> { let ram_pointer = self.op_stack.peek_at(ST1); - let ram_value = self.op_stack.pop()?; + let (ram_value, _) = self.op_stack.pop()?; self.ram_pointer = ram_pointer.value(); self.ram.insert(ram_pointer, ram_value); self.instruction_pointer += 1; @@ -366,7 +366,7 @@ impl<'pgm> VMState<'pgm> { } fn hash(&mut self) -> Result> { - let to_hash = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let (to_hash, _) = self.op_stack.pop_multiple::<{ 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); @@ -392,7 +392,7 @@ impl<'pgm> VMState<'pgm> { fn sponge_absorb(&mut self) -> Result> { // fetch top elements but don't alter the stack - let to_absorb = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let (to_absorb, _) = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; for i in (0..tip5::RATE).rev() { self.op_stack.push(to_absorb[i]); } @@ -425,9 +425,9 @@ impl<'pgm> VMState<'pgm> { fn divine_sibling(&mut self) -> Result> { let _st0_through_st4 = self.op_stack.pop_multiple::<{ DIGEST_LENGTH }>()?; - let known_digest = self.op_stack.pop_multiple()?; + let (known_digest, _) = self.op_stack.pop_multiple()?; - let node_index = self.op_stack.pop_u32()?; + let (node_index, _) = self.op_stack.pop_u32()?; let parent_node_index = node_index / 2; self.op_stack.push(parent_node_index.into()); @@ -470,23 +470,23 @@ impl<'pgm> VMState<'pgm> { } fn add(&mut self) -> Result> { - let lhs = self.op_stack.pop()?; - let rhs = self.op_stack.pop()?; + let (lhs, _) = self.op_stack.pop()?; + let (rhs, _) = self.op_stack.pop()?; self.op_stack.push(lhs + rhs); self.instruction_pointer += 1; Ok(vec![]) } fn mul(&mut self) -> Result> { - let lhs = self.op_stack.pop()?; - let rhs = self.op_stack.pop()?; + let (lhs, _) = self.op_stack.pop()?; + let (rhs, _) = self.op_stack.pop()?; self.op_stack.push(lhs * rhs); self.instruction_pointer += 1; Ok(vec![]) } fn invert(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop()?; + let (top_of_stack, _) = self.op_stack.pop()?; if top_of_stack.is_zero() { bail!(InverseOfZero); } @@ -496,8 +496,8 @@ impl<'pgm> VMState<'pgm> { } fn eq(&mut self) -> Result> { - let lhs = self.op_stack.pop()?; - let rhs = self.op_stack.pop()?; + let (lhs, _) = self.op_stack.pop()?; + let (rhs, _) = self.op_stack.pop()?; let eq: u32 = (lhs == rhs).into(); self.op_stack.push(eq.into()); self.instruction_pointer += 1; @@ -505,7 +505,7 @@ impl<'pgm> VMState<'pgm> { } fn split(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop()?; + let (top_of_stack, _) = self.op_stack.pop()?; let lo = BFieldElement::new(top_of_stack.value() & 0xffff_ffff); let hi = BFieldElement::new(top_of_stack.value() >> 32); self.op_stack.push(hi); @@ -519,8 +519,8 @@ impl<'pgm> VMState<'pgm> { } fn lt(&mut self) -> Result> { - let lhs = self.op_stack.pop_u32()?; - let rhs = self.op_stack.pop_u32()?; + let (lhs, _) = self.op_stack.pop_u32()?; + let (rhs, _) = self.op_stack.pop_u32()?; let lt: u32 = (lhs < rhs).into(); self.op_stack.push(lt.into()); @@ -532,8 +532,8 @@ impl<'pgm> VMState<'pgm> { } fn and(&mut self) -> Result> { - let lhs = self.op_stack.pop_u32()?; - let rhs = self.op_stack.pop_u32()?; + let (lhs, _) = self.op_stack.pop_u32()?; + let (rhs, _) = self.op_stack.pop_u32()?; let and = lhs & rhs; self.op_stack.push(and.into()); @@ -545,8 +545,8 @@ impl<'pgm> VMState<'pgm> { } fn xor(&mut self) -> Result> { - let lhs = self.op_stack.pop_u32()?; - let rhs = self.op_stack.pop_u32()?; + let (lhs, _) = self.op_stack.pop_u32()?; + let (rhs, _) = self.op_stack.pop_u32()?; let xor = lhs ^ rhs; self.op_stack.push(xor.into()); @@ -561,7 +561,7 @@ impl<'pgm> VMState<'pgm> { } fn log_2_floor(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop_u32()?; + let (top_of_stack, _) = self.op_stack.pop_u32()?; if top_of_stack.is_zero() { bail!(LogarithmOfZero); } @@ -576,8 +576,8 @@ impl<'pgm> VMState<'pgm> { } fn pow(&mut self) -> Result> { - let base = self.op_stack.pop()?; - let exponent = self.op_stack.pop_u32()?; + let (base, _) = self.op_stack.pop()?; + let (exponent, _) = self.op_stack.pop_u32()?; let base_pow_exponent = base.mod_pow(exponent.into()); self.op_stack.push(base_pow_exponent); @@ -590,8 +590,8 @@ impl<'pgm> VMState<'pgm> { } fn div_mod(&mut self) -> Result> { - let numerator = self.op_stack.pop_u32()?; - let denominator = self.op_stack.pop_u32()?; + let (numerator, _) = self.op_stack.pop_u32()?; + let (denominator, _) = self.op_stack.pop_u32()?; if denominator.is_zero() { bail!(DivisionByZero); } @@ -613,7 +613,7 @@ impl<'pgm> VMState<'pgm> { } fn pop_count(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop_u32()?; + let (top_of_stack, _) = self.op_stack.pop_u32()?; let pop_count = top_of_stack.count_ones(); self.op_stack.push(pop_count.into()); @@ -625,7 +625,7 @@ impl<'pgm> VMState<'pgm> { } fn xx_add(&mut self) -> Result> { - let lhs = self.op_stack.pop_extension_field_element()?; + let (lhs, _) = self.op_stack.pop_extension_field_element()?; let rhs = self.op_stack.peek_at_top_extension_field_element(); self.op_stack.push_extension_field_element(lhs + rhs); self.instruction_pointer += 1; @@ -633,7 +633,7 @@ impl<'pgm> VMState<'pgm> { } fn xx_mul(&mut self) -> Result> { - let lhs = self.op_stack.pop_extension_field_element()?; + let (lhs, _) = self.op_stack.pop_extension_field_element()?; let rhs = self.op_stack.peek_at_top_extension_field_element(); self.op_stack.push_extension_field_element(lhs * rhs); self.instruction_pointer += 1; @@ -641,7 +641,7 @@ impl<'pgm> VMState<'pgm> { } fn x_invert(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop_extension_field_element()?; + let (top_of_stack, _) = self.op_stack.pop_extension_field_element()?; if top_of_stack.is_zero() { bail!(InverseOfZero); } @@ -652,15 +652,15 @@ impl<'pgm> VMState<'pgm> { } fn xb_mul(&mut self) -> Result> { - let lhs = self.op_stack.pop()?; - let rhs = self.op_stack.pop_extension_field_element()?; + let (lhs, _) = self.op_stack.pop()?; + let (rhs, _) = self.op_stack.pop_extension_field_element()?; self.op_stack.push_extension_field_element(lhs.lift() * rhs); self.instruction_pointer += 1; Ok(vec![]) } fn write_io(&mut self) -> Result> { - let top_of_stack = self.op_stack.pop()?; + let (top_of_stack, _) = self.op_stack.pop()?; self.public_output.push(top_of_stack); self.instruction_pointer += 1; Ok(vec![]) @@ -717,7 +717,7 @@ impl<'pgm> VMState<'pgm> { processor_row[ST14.base_table_index()] = self.op_stack.peek_at(OpStackElement::ST14); processor_row[ST15.base_table_index()] = self.op_stack.peek_at(OpStackElement::ST15); processor_row[OSP.base_table_index()] = self.op_stack.op_stack_pointer(); - processor_row[OSV.base_table_index()] = self.op_stack.op_stack_value(); + processor_row[OSV.base_table_index()] = self.op_stack.first_underflow_element(); processor_row[HV0.base_table_index()] = helper_variables[0]; processor_row[HV1.base_table_index()] = helper_variables[1]; processor_row[HV2.base_table_index()] = helper_variables[2]; From 2aa72e77239a4006711a60dfb1d07b26a89e99c9 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 19 Oct 2023 12:44:27 +0200 Subject: [PATCH 09/27] fix: overflowing subtractions when accessing op stack underflow --- triton-vm/src/op_stack.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 6315b6ec6..2a40bb16e 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -133,9 +133,14 @@ impl OpStack { /// The first element of the op-stack underflow memory, or 0 if the op-stack underflow memory /// is empty. pub(crate) fn first_underflow_element(&self) -> BFieldElement { - let top_of_stack_index = self.stack.len() - 1; - let underflow_start = top_of_stack_index - OpStackElement::COUNT; - self.stack.get(underflow_start).copied().unwrap_or_default() + let default = BFieldElement::zero(); + let Some(top_of_stack_index) = self.stack.len().checked_sub(1) else { + return default; + }; + let Some(underflow_start) = top_of_stack_index.checked_sub(OpStackElement::COUNT) else { + return default; + }; + self.stack.get(underflow_start).copied().unwrap_or(default) } } @@ -392,6 +397,16 @@ mod tests { assert!(op_stack.is_too_shallow()); } + #[test] + fn trying_to_access_first_underflow_element_never_panics() { + let mut op_stack = OpStack::new(Default::default()); + let way_too_long = 2 * op_stack.stack.len(); + for _ in 0..way_too_long { + let _ = op_stack.pop(); + let _ = op_stack.first_underflow_element(); + } + } + #[test] fn canonicalize_empty_underflow_io_sequence() { let mut sequence = vec![]; From 3ee0a011348961cf507108de8ccfe609363a9863 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Thu, 19 Oct 2023 13:52:40 +0200 Subject: [PATCH 10/27] feat: propagate opstack underflow read/write to AET --- triton-vm/src/aet.rs | 1 + triton-vm/src/op_stack.rs | 47 ++++- triton-vm/src/table/op_stack_table.rs | 130 ++++++++++++ triton-vm/src/vm.rs | 282 ++++++++++++++++++-------- 4 files changed, 366 insertions(+), 94 deletions(-) diff --git a/triton-vm/src/aet.rs b/triton-vm/src/aet.rs index 0b20c9b06..2fe1e7fad 100644 --- a/triton-vm/src/aet.rs +++ b/triton-vm/src/aet.rs @@ -223,6 +223,7 @@ impl AlgebraicExecutionTrace { SpongeStateReset => self.append_initial_sponge_state(), Tip5Trace(instruction, trace) => self.append_sponge_trace(instruction, *trace), U32Call(u32_entry) => self.record_u32_table_entry(u32_entry), + OpStackCall(_) => (), // todo } } diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 2a40bb16e..901f2874e 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -5,6 +5,7 @@ use std::fmt::Result as FmtResult; use anyhow::anyhow; use anyhow::bail; use anyhow::Result; +use arbitrary::Arbitrary; use get_size::GetSize; use itertools::Itertools; use num_traits::Zero; @@ -145,8 +146,9 @@ impl OpStack { } /// Indicates changes to the op-stack underflow memory. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize)] -pub(crate) enum UnderflowIO { +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, GetSize, Serialize, Deserialize, Arbitrary)] +#[must_use = "The change to underflow memory should be handled."] +pub enum UnderflowIO { Read(BFieldElement), Write(BFieldElement), } @@ -157,10 +159,10 @@ impl UnderflowIO { /// For example, the sequence `[Read(5), Write(5), Read(7)]` can be replaced with `[Read(7)]`. /// Similarly, the sequence `[Write(5), Write(3), Read(3), Read(5), Write(7)]` can be replaced /// with `[Write(7)]`. - pub(crate) fn canonicalize_sequence(sequence: &mut Vec) { + pub fn canonicalize_sequence(sequence: &mut Vec) { while let Some(index) = Self::index_of_dual_pair(sequence) { - sequence.remove(index); - sequence.remove(index); + let _ = sequence.remove(index); + let _ = sequence.remove(index); } } @@ -179,6 +181,20 @@ impl UnderflowIO { _ => false, } } + + pub fn shrinks_stack(&self) -> bool { + match self { + Self::Read(_) => true, + Self::Write(_) => false, + } + } + + pub fn grows_stack(&self) -> bool { + match self { + Self::Read(_) => false, + Self::Write(_) => true, + } + } } /// Represents the [`OpStack`] registers directly accessible by Triton VM. @@ -315,6 +331,8 @@ impl From<&OpStackElement> for BFieldElement { #[cfg(test)] mod tests { + use proptest::prelude::*; + use proptest_arbitrary_interop::arb; use twenty_first::shared_math::b_field_element::BFieldElement; use super::*; @@ -333,7 +351,7 @@ mod tests { // push elements 1 thru 17 for i in 1..=17 { - op_stack.push(BFieldElement::new(i as u64)); + let _ = op_stack.push(BFieldElement::new(i as u64)); } // verify height @@ -371,7 +389,7 @@ mod tests { // pop 11 elements for _ in 0..11 { - op_stack.pop().expect("can't pop"); + let _ = op_stack.pop().expect("can't pop"); } // verify height @@ -382,8 +400,8 @@ mod tests { ); // pop 2 XFieldElements - op_stack.pop_extension_field_element().expect("can't pop"); - op_stack.pop_extension_field_element().expect("can't pop"); + let _ = op_stack.pop_extension_field_element().expect("can't pop"); + let _ = op_stack.pop_extension_field_element().expect("can't pop"); // verify height assert_eq!(op_stack.stack.len(), 16); @@ -393,7 +411,7 @@ mod tests { ); // verify underflow - op_stack.pop().expect("can't pop"); + let _ = op_stack.pop().expect("can't pop"); assert!(op_stack.is_too_shallow()); } @@ -443,4 +461,13 @@ mod tests { let expected_sequence = vec![UnderflowIO::Write(7_u64.into())]; assert_eq!(expected_sequence, sequence); } + + proptest! { + #[test] + fn underflow_io_either_shrinks_stack_or_grows_stack(underflow_io in arb::()) { + let shrinks_stack = underflow_io.shrinks_stack(); + let grows_stack = underflow_io.grows_stack(); + assert!(shrinks_stack ^ grows_stack); + } + } } diff --git a/triton-vm/src/table/op_stack_table.rs b/triton-vm/src/table/op_stack_table.rs index 9f29b464b..fc6fbefa1 100644 --- a/triton-vm/src/table/op_stack_table.rs +++ b/triton-vm/src/table/op_stack_table.rs @@ -1,5 +1,6 @@ use std::cmp::Ordering; +use arbitrary::Arbitrary; use ndarray::parallel::prelude::*; use ndarray::s; use ndarray::Array1; @@ -14,6 +15,7 @@ use twenty_first::shared_math::x_field_element::XFieldElement; use crate::aet::AlgebraicExecutionTrace; use crate::op_stack::OpStackElement; +use crate::op_stack::UnderflowIO; use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; use crate::table::constraint_circuit::DualRowIndicator::*; @@ -34,6 +36,51 @@ pub struct OpStackTable {} #[derive(Debug, Clone)] pub struct ExtOpStackTable {} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Arbitrary)] +pub struct OpStackTableEntry { + pub clk: u32, + pub op_stack_pointer: BFieldElement, + pub underflow_io: UnderflowIO, +} + +impl OpStackTableEntry { + pub fn new(clk: u32, op_stack_pointer: BFieldElement, underflow_io: UnderflowIO) -> Self { + Self { + clk, + op_stack_pointer, + underflow_io, + } + } + + pub fn shrinks_stack(&self) -> bool { + self.underflow_io.shrinks_stack() + } + + pub fn grows_stack(&self) -> bool { + self.underflow_io.grows_stack() + } + + pub fn from_underflow_io_sequence( + clk: u32, + mut op_stack_pointer: BFieldElement, + mut underflow_io_sequence: Vec, + ) -> Vec { + UnderflowIO::canonicalize_sequence(&mut underflow_io_sequence); + let mut op_stack_table_entries = vec![]; + for underflow_io in underflow_io_sequence { + if underflow_io.shrinks_stack() { + op_stack_pointer.decrement(); + } + let op_stack_table_entry = Self::new(clk, op_stack_pointer, underflow_io); + op_stack_table_entries.push(op_stack_table_entry); + if underflow_io.grows_stack() { + op_stack_pointer.increment(); + } + } + op_stack_table_entries + } +} + impl ExtOpStackTable { pub fn initial_constraints( circuit_builder: &ConstraintCircuitBuilder, @@ -329,7 +376,13 @@ impl OpStackTable { #[cfg(test)] pub(crate) mod tests { + use std::cmp::max; + + use itertools::Itertools; use num_traits::Zero; + use proptest::collection::vec; + use proptest::prelude::*; + use proptest_arbitrary_interop::arb; use super::*; @@ -420,4 +473,81 @@ pub(crate) mod tests { true } + + proptest! { + #[test] + fn op_stack_table_entry_either_shrinks_stack_or_grows_stack( + entry in arb::() + ) { + let shrinks_stack = entry.shrinks_stack(); + let grows_stack = entry.grows_stack(); + assert!(shrinks_stack ^ grows_stack); + } + } + + proptest! { + #[test] + fn op_stack_pointer_in_sequence_of_op_stack_table_entries( + clk: u32, + osp in OpStackElement::COUNT..1024, + base_field_elements in vec(arb::(), 0..32), + sequence_of_writes: bool, + ) { + let sequence_length = base_field_elements.len(); + let osp = match sequence_of_writes { + true => osp, + false => max(osp, sequence_length), + }; + + let sequence_length = u64::try_from(sequence_length).unwrap(); + let osp = u64::try_from(osp).unwrap(); + + let underflow_io_operation = match sequence_of_writes { + true => UnderflowIO::Write, + false => UnderflowIO::Read, + }; + let underflow_io = base_field_elements + .into_iter() + .map(underflow_io_operation) + .collect(); + + let op_stack_pointer = osp.into(); + let entries = + OpStackTableEntry::from_underflow_io_sequence(clk, op_stack_pointer, underflow_io); + let op_stack_pointers = entries + .iter() + .map(|entry| entry.op_stack_pointer.value()) + .sorted() + .collect_vec(); + + let expected_lowest_osp = match sequence_of_writes { + true => osp, + false => osp - sequence_length, + }; + let expected_largest_osp = match sequence_of_writes { + true => osp + sequence_length, + false => osp, + }; + let expected_op_stack_pointers = + (expected_lowest_osp..expected_largest_osp).collect_vec(); + + prop_assert_eq!(expected_op_stack_pointers, op_stack_pointers); + } + } + + proptest! { + #[test] + fn clk_stays_same_in_sequence_of_op_stack_table_entries( + clk: u32, + osp in OpStackElement::COUNT..1024, + underflow_io in vec(arb::(), 0..OpStackElement::COUNT), + ) { + let op_stack_pointer = u64::try_from(osp).unwrap().into(); + let entries = + OpStackTableEntry::from_underflow_io_sequence(clk, op_stack_pointer, underflow_io); + let clk_values = entries.iter().map(|entry| entry.clk).collect_vec(); + let all_clk_values_are_clk = clk_values.iter().all(|&c| c == clk); + prop_assert!(all_clk_values_are_clk); + } + } } diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 4b728e5e8..eea8f529d 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -29,6 +29,7 @@ use crate::op_stack::*; use crate::program::*; use crate::stark::StarkHasher; use crate::table::hash_table::PermutationTrace; +use crate::table::op_stack_table::OpStackTableEntry; use crate::table::processor_table; use crate::table::processor_table::ProcessorTraceRow; use crate::table::table_column::*; @@ -100,6 +101,8 @@ pub enum CoProcessorCall { Tip5Trace(Instruction, Box), U32Call(U32TableEntry), + + OpStackCall(OpStackTableEntry), } impl<'pgm> VMState<'pgm> { @@ -257,32 +260,59 @@ impl<'pgm> VMState<'pgm> { Ok(co_processor_calls) } + fn underflow_io_sequence_to_co_processor_calls( + &self, + underflow_io_sequence: Vec, + ) -> Vec { + let op_stack_table_entries = OpStackTableEntry::from_underflow_io_sequence( + self.cycle_count, + self.op_stack.op_stack_pointer(), + underflow_io_sequence, + ); + op_stack_table_entries + .into_iter() + .map(OpStackCall) + .collect() + } + fn pop(&mut self) -> Result> { - self.op_stack.pop()?; + let (_, underflow_io) = self.op_stack.pop()?; + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn push(&mut self, element: BFieldElement) -> Vec { - self.op_stack.push(element); + let underflow_io = self.op_stack.push(element); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 2; - vec![] + op_stack_calls } fn divine(&mut self) -> Result> { let element = self.secret_individual_tokens.pop_front().ok_or(anyhow!( "Instruction `divine`: secret input buffer is empty." ))?; - self.op_stack.push(element); + let underflow_io = self.op_stack.push(element); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn dup(&mut self, stack_register: OpStackElement) -> Vec { let element = self.op_stack.peek_at(stack_register); - self.op_stack.push(element); + let underflow_io = self.op_stack.push(element); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 2; - vec![] + op_stack_calls } fn swap(&mut self, stack_register: OpStackElement) -> Result> { @@ -300,12 +330,15 @@ impl<'pgm> VMState<'pgm> { } fn skiz(&mut self) -> Result> { - let (top_of_stack, _) = self.op_stack.pop()?; + let (top_of_stack, underflow_io) = self.op_stack.pop()?; + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += match top_of_stack.is_zero() { true => 1 + self.next_instruction()?.size(), false => 1, }; - Ok(vec![]) + Ok(op_stack_calls) } fn call(&mut self, call_destination: BFieldElement) -> Vec { @@ -331,14 +364,18 @@ impl<'pgm> VMState<'pgm> { } fn assert(&mut self) -> Result> { - let (top_of_stack, _) = self.op_stack.pop()?; + let (top_of_stack, underflow_io) = self.op_stack.pop()?; + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + if !top_of_stack.is_one() { let assertion_failed = AssertionFailed(self.instruction_pointer, self.cycle_count, top_of_stack); bail!(assertion_failed); } + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn halt(&mut self) -> Vec { @@ -349,36 +386,50 @@ impl<'pgm> VMState<'pgm> { fn read_mem(&mut self) -> Vec { let ram_pointer = self.op_stack.peek_at(ST0); - let ram_value = self.memory_get(&ram_pointer); - self.op_stack.push(ram_value); self.ram_pointer = ram_pointer.value(); + + let ram_value = self.memory_get(&ram_pointer); + let underflow_io = self.op_stack.push(ram_value); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 1; - vec![] + op_stack_calls } fn write_mem(&mut self) -> Result> { let ram_pointer = self.op_stack.peek_at(ST1); - let (ram_value, _) = self.op_stack.pop()?; + let (ram_value, underflow_io) = self.op_stack.pop()?; self.ram_pointer = ram_pointer.value(); self.ram.insert(ram_pointer, ram_value); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn hash(&mut self) -> Result> { - let (to_hash, _) = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let (to_hash, underflow_io) = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let mut all_underflow_io = underflow_io.to_vec(); + 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]); + let underflow_io = self.op_stack.push(hash_output[i]); + all_underflow_io.push(underflow_io); } for _ in 0..DIGEST_LENGTH { - self.op_stack.push(BFieldElement::zero()); + let underflow_io = self.op_stack.push(BFieldElement::zero()); + all_underflow_io.push(underflow_io); } - let co_processor_calls = vec![Tip5Trace(Hash, Box::new(tip5_trace))]; + + let mut co_processor_calls = + self.underflow_io_sequence_to_co_processor_calls(all_underflow_io); + co_processor_calls.push(Tip5Trace(Hash, Box::new(tip5_trace))); self.instruction_pointer += 1; Ok(co_processor_calls) @@ -392,9 +443,11 @@ impl<'pgm> VMState<'pgm> { fn sponge_absorb(&mut self) -> Result> { // fetch top elements but don't alter the stack - let (to_absorb, _) = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let (to_absorb, underflow_io) = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let mut all_underflow_io = underflow_io.to_vec(); for i in (0..tip5::RATE).rev() { - self.op_stack.push(to_absorb[i]); + let underflow_io = self.op_stack.push(to_absorb[i]); + all_underflow_io.push(underflow_io); } self.sponge_state[..tip5::RATE].copy_from_slice(&to_absorb); @@ -402,47 +455,65 @@ impl<'pgm> VMState<'pgm> { state: self.sponge_state, }); self.sponge_state = tip5_trace.last().unwrap().to_owned(); - let co_processor_calls = vec![Tip5Trace(SpongeAbsorb, Box::new(tip5_trace))]; + + let mut co_processor_calls = + self.underflow_io_sequence_to_co_processor_calls(all_underflow_io); + co_processor_calls.push(Tip5Trace(SpongeAbsorb, Box::new(tip5_trace))); self.instruction_pointer += 1; Ok(co_processor_calls) } fn sponge_squeeze(&mut self) -> Result> { - let _ = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let (_, underflow_io) = self.op_stack.pop_multiple::<{ tip5::RATE }>()?; + let mut all_underflow_io = underflow_io.to_vec(); for i in (0..tip5::RATE).rev() { - self.op_stack.push(self.sponge_state[i]); + let underflow_io = self.op_stack.push(self.sponge_state[i]); + all_underflow_io.push(underflow_io); } let tip5_trace = Tip5::trace(&mut Tip5State { state: self.sponge_state, }); self.sponge_state = tip5_trace.last().unwrap().to_owned(); - let co_processor_calls = vec![Tip5Trace(SpongeSqueeze, Box::new(tip5_trace))]; + + let mut co_processor_calls = + self.underflow_io_sequence_to_co_processor_calls(all_underflow_io); + co_processor_calls.push(Tip5Trace(SpongeSqueeze, Box::new(tip5_trace))); self.instruction_pointer += 1; Ok(co_processor_calls) } fn divine_sibling(&mut self) -> Result> { - let _st0_through_st4 = self.op_stack.pop_multiple::<{ DIGEST_LENGTH }>()?; - let (known_digest, _) = self.op_stack.pop_multiple()?; + let (_st0_through_st4, underflow_io) = self.op_stack.pop_multiple::<{ DIGEST_LENGTH }>()?; + let mut all_underflow_io = underflow_io.to_vec(); + let (known_digest, underflow_io) = self.op_stack.pop_multiple()?; + all_underflow_io.extend(underflow_io); + + let (node_index, underflow_io) = self.op_stack.pop_u32()?; + all_underflow_io.push(underflow_io); - let (node_index, _) = self.op_stack.pop_u32()?; let parent_node_index = node_index / 2; - self.op_stack.push(parent_node_index.into()); + let underflow_io = self.op_stack.push(parent_node_index.into()); + all_underflow_io.push(underflow_io); let sibling_digest = self.pop_secret_digest()?; let (left_digest, right_digest) = Self::put_known_digest_on_correct_side(node_index, known_digest, sibling_digest); for &digest_element in right_digest.iter().rev() { - self.op_stack.push(digest_element); + let underflow_io = self.op_stack.push(digest_element); + all_underflow_io.push(underflow_io); } for &digest_element in left_digest.iter().rev() { - self.op_stack.push(digest_element); + let underflow_io = self.op_stack.push(digest_element); + all_underflow_io.push(underflow_io); } + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn assert_vector(&mut self) -> Result> { @@ -470,19 +541,27 @@ impl<'pgm> VMState<'pgm> { } fn add(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop()?; - let (rhs, _) = self.op_stack.pop()?; - self.op_stack.push(lhs + rhs); + let (lhs, underflow_io_lhs) = self.op_stack.pop()?; + let (rhs, underflow_io_rhs) = self.op_stack.pop()?; + let underflow_io_result = self.op_stack.push(lhs + rhs); + + let underflow_io = vec![underflow_io_lhs, underflow_io_rhs, underflow_io_result]; + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn mul(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop()?; - let (rhs, _) = self.op_stack.pop()?; - self.op_stack.push(lhs * rhs); + let (lhs, underflow_io_lhs) = self.op_stack.pop()?; + let (rhs, underflow_io_rhs) = self.op_stack.pop()?; + let underflow_io_result = self.op_stack.push(lhs * rhs); + + let underflow_io = vec![underflow_io_lhs, underflow_io_rhs, underflow_io_result]; + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn invert(&mut self) -> Result> { @@ -490,100 +569,128 @@ impl<'pgm> VMState<'pgm> { if top_of_stack.is_zero() { bail!(InverseOfZero); } - self.op_stack.push(top_of_stack.inverse()); + let _ = self.op_stack.push(top_of_stack.inverse()); self.instruction_pointer += 1; Ok(vec![]) } fn eq(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop()?; - let (rhs, _) = self.op_stack.pop()?; + let (lhs, underflow_io_lhs) = self.op_stack.pop()?; + let (rhs, underflow_io_rhs) = self.op_stack.pop()?; let eq: u32 = (lhs == rhs).into(); - self.op_stack.push(eq.into()); + let underflow_io_eq = self.op_stack.push(eq.into()); + + let underflow_io = vec![underflow_io_lhs, underflow_io_rhs, underflow_io_eq]; + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn split(&mut self) -> Result> { - let (top_of_stack, _) = self.op_stack.pop()?; + let (top_of_stack, underflow_io_top) = self.op_stack.pop()?; let lo = BFieldElement::new(top_of_stack.value() & 0xffff_ffff); let hi = BFieldElement::new(top_of_stack.value() >> 32); - self.op_stack.push(hi); - self.op_stack.push(lo); + let underflow_io_hi = self.op_stack.push(hi); + let underflow_io_lo = self.op_stack.push(lo); + + let underflow_io = vec![underflow_io_top, underflow_io_hi, underflow_io_lo]; + let mut co_processor_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); let u32_table_entry = U32TableEntry::new_from_base_field_element(Split, lo, hi); - let co_processor_calls = vec![U32Call(u32_table_entry)]; + let u32_call = U32Call(u32_table_entry); + co_processor_calls.push(u32_call); self.instruction_pointer += 1; Ok(co_processor_calls) } fn lt(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop_u32()?; - let (rhs, _) = self.op_stack.pop_u32()?; + let (lhs, underflow_io_lhs) = self.op_stack.pop_u32()?; + let (rhs, underflow_io_rhs) = self.op_stack.pop_u32()?; let lt: u32 = (lhs < rhs).into(); - self.op_stack.push(lt.into()); + let underflow_io_result = self.op_stack.push(lt.into()); + + let underflow_io = vec![underflow_io_lhs, underflow_io_rhs, underflow_io_result]; + let mut co_processor_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); let u32_table_entry = U32TableEntry::new(Lt, lhs, rhs); - let co_processor_calls = vec![U32Call(u32_table_entry)]; + let u32_call = U32Call(u32_table_entry); + co_processor_calls.push(u32_call); self.instruction_pointer += 1; Ok(co_processor_calls) } fn and(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop_u32()?; - let (rhs, _) = self.op_stack.pop_u32()?; + let (lhs, underflow_io_lhs) = self.op_stack.pop_u32()?; + let (rhs, underflow_io_rhs) = self.op_stack.pop_u32()?; let and = lhs & rhs; - self.op_stack.push(and.into()); + let underflow_io_and = self.op_stack.push(and.into()); + + let underflow_io = vec![underflow_io_lhs, underflow_io_rhs, underflow_io_and]; + let mut co_processor_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); let u32_table_entry = U32TableEntry::new(And, lhs, rhs); - let co_processor_calls = vec![U32Call(u32_table_entry)]; + let u32_call = U32Call(u32_table_entry); + co_processor_calls.push(u32_call); self.instruction_pointer += 1; Ok(co_processor_calls) } fn xor(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop_u32()?; - let (rhs, _) = self.op_stack.pop_u32()?; + let (lhs, underflow_io_lhs) = self.op_stack.pop_u32()?; + let (rhs, underflow_io_rhs) = self.op_stack.pop_u32()?; let xor = lhs ^ rhs; - self.op_stack.push(xor.into()); + let underflow_io_xor = self.op_stack.push(xor.into()); + + let underflow_io = vec![underflow_io_lhs, underflow_io_rhs, underflow_io_xor]; + let mut co_processor_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); // Triton VM uses the following equality to compute the results of both the `and` // and `xor` instruction using the u32 coprocessor's `and` capability: // a ^ b = a + b - 2 · (a & b) let u32_table_entry = U32TableEntry::new(And, lhs, rhs); - let co_processor_calls = vec![U32Call(u32_table_entry)]; + let u32_call = U32Call(u32_table_entry); + co_processor_calls.push(u32_call); self.instruction_pointer += 1; Ok(co_processor_calls) } fn log_2_floor(&mut self) -> Result> { - let (top_of_stack, _) = self.op_stack.pop_u32()?; + let (top_of_stack, underflow_io_top) = self.op_stack.pop_u32()?; if top_of_stack.is_zero() { bail!(LogarithmOfZero); } let log_2_floor = top_of_stack.ilog2(); - self.op_stack.push(log_2_floor.into()); + let underflow_io_result = self.op_stack.push(log_2_floor.into()); + + let underflow_io = vec![underflow_io_top, underflow_io_result]; + let mut co_processor_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); let u32_table_entry = U32TableEntry::new(Log2Floor, top_of_stack, 0); - let co_processor_calls = vec![U32Call(u32_table_entry)]; + let u32_call = U32Call(u32_table_entry); + co_processor_calls.push(u32_call); self.instruction_pointer += 1; Ok(co_processor_calls) } fn pow(&mut self) -> Result> { - let (base, _) = self.op_stack.pop()?; - let (exponent, _) = self.op_stack.pop_u32()?; + let (base, underflow_io_base) = self.op_stack.pop()?; + let (exponent, underflow_io_exp) = self.op_stack.pop_u32()?; let base_pow_exponent = base.mod_pow(exponent.into()); - self.op_stack.push(base_pow_exponent); + let underflow_io_result = self.op_stack.push(base_pow_exponent); + + let underflow_io = vec![underflow_io_base, underflow_io_exp, underflow_io_result]; + let mut co_processor_calls = self.underflow_io_sequence_to_co_processor_calls(underflow_io); let u32_table_entry = U32TableEntry::new_from_base_field_element(Pow, base, exponent.into()); - let co_processor_calls = vec![U32Call(u32_table_entry)]; + let u32_call = U32Call(u32_table_entry); + co_processor_calls.push(u32_call); self.instruction_pointer += 1; Ok(co_processor_calls) @@ -598,8 +705,8 @@ impl<'pgm> VMState<'pgm> { let quotient = numerator / denominator; let remainder = numerator % denominator; - self.op_stack.push(quotient.into()); - self.op_stack.push(remainder.into()); + let _ = self.op_stack.push(quotient.into()); + let _ = self.op_stack.push(remainder.into()); let remainder_is_less_than_denominator = U32TableEntry::new(Lt, remainder, denominator); let numerator_and_quotient_range_check = U32TableEntry::new(Split, numerator, quotient); @@ -615,7 +722,7 @@ impl<'pgm> VMState<'pgm> { fn pop_count(&mut self) -> Result> { let (top_of_stack, _) = self.op_stack.pop_u32()?; let pop_count = top_of_stack.count_ones(); - self.op_stack.push(pop_count.into()); + let _ = self.op_stack.push(pop_count.into()); let u32_table_entry = U32TableEntry::new(PopCount, top_of_stack, 0); let co_processor_calls = vec![U32Call(u32_table_entry)]; @@ -627,7 +734,7 @@ impl<'pgm> VMState<'pgm> { fn xx_add(&mut self) -> Result> { let (lhs, _) = self.op_stack.pop_extension_field_element()?; let rhs = self.op_stack.peek_at_top_extension_field_element(); - self.op_stack.push_extension_field_element(lhs + rhs); + let _ = self.op_stack.push_extension_field_element(lhs + rhs); self.instruction_pointer += 1; Ok(vec![]) } @@ -635,7 +742,7 @@ impl<'pgm> VMState<'pgm> { fn xx_mul(&mut self) -> Result> { let (lhs, _) = self.op_stack.pop_extension_field_element()?; let rhs = self.op_stack.peek_at_top_extension_field_element(); - self.op_stack.push_extension_field_element(lhs * rhs); + let _ = self.op_stack.push_extension_field_element(lhs * rhs); self.instruction_pointer += 1; Ok(vec![]) } @@ -645,34 +752,41 @@ impl<'pgm> VMState<'pgm> { if top_of_stack.is_zero() { bail!(InverseOfZero); } - self.op_stack - .push_extension_field_element(top_of_stack.inverse()); + let inverse = top_of_stack.inverse(); + let _ = self.op_stack.push_extension_field_element(inverse); self.instruction_pointer += 1; Ok(vec![]) } fn xb_mul(&mut self) -> Result> { - let (lhs, _) = self.op_stack.pop()?; + let (lhs, underflow_io) = self.op_stack.pop()?; let (rhs, _) = self.op_stack.pop_extension_field_element()?; - self.op_stack.push_extension_field_element(lhs.lift() * rhs); + let _ = self.op_stack.push_extension_field_element(lhs.lift() * rhs); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); + self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn write_io(&mut self) -> Result> { - let (top_of_stack, _) = self.op_stack.pop()?; + let (top_of_stack, underflow_io) = self.op_stack.pop()?; self.public_output.push(top_of_stack); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } fn read_io(&mut self) -> Result> { let read_element = self.public_input.pop_front().ok_or(anyhow!( "Instruction `read_io`: public input buffer is empty." ))?; - self.op_stack.push(read_element); + let underflow_io = self.op_stack.push(read_element); + + let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); self.instruction_pointer += 1; - Ok(vec![]) + Ok(op_stack_calls) } pub fn to_processor_row(&self) -> Array1 { From a57ef7c33980cfa20b0e962802f7abb8b8420efd Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 20 Oct 2023 10:45:01 +0200 Subject: [PATCH 11/27] feat: record opstack underflow read/write in AET --- triton-vm/src/aet.rs | 20 +++++++++++++++++--- triton-vm/src/op_stack.rs | 7 +++++++ triton-vm/src/table/master_table.rs | 24 +++++++++++++++++------- triton-vm/src/table/op_stack_table.rs | 16 ++++++++++++++++ 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/triton-vm/src/aet.rs b/triton-vm/src/aet.rs index 2fe1e7fad..1682d36f6 100644 --- a/triton-vm/src/aet.rs +++ b/triton-vm/src/aet.rs @@ -23,13 +23,13 @@ use crate::error::InstructionError::InstructionPointerOverflow; use crate::instruction::Instruction; use crate::program::Program; use crate::stark::StarkHasher; -use crate::table::hash_table; use crate::table::hash_table::HashTable; use crate::table::hash_table::PermutationTrace; -use crate::table::processor_table; +use crate::table::op_stack_table::OpStackTableEntry; use crate::table::table_column::HashBaseTableColumn::CI; use crate::table::table_column::MasterBaseTableColumn; use crate::table::u32_table::U32TableEntry; +use crate::table::*; use crate::vm::CoProcessorCall; use crate::vm::CoProcessorCall::*; use crate::vm::VMState; @@ -53,6 +53,8 @@ pub struct AlgebraicExecutionTrace { /// Records the state of the processor after each instruction. pub processor_trace: Array2, + pub op_stack_underflow_trace: Array2, + /// The trace of hashing the program whose execution generated this `AlgebraicExecutionTrace`. /// The resulting digest /// 1. ties a [`Proof`](crate::proof::Proof) to the program it was produced from, and @@ -87,6 +89,7 @@ impl AlgebraicExecutionTrace { program, instruction_multiplicities: vec![0_u32; program_len], processor_trace: Array2::default([0, processor_table::BASE_WIDTH]), + op_stack_underflow_trace: Array2::default([0, op_stack_table::BASE_WIDTH]), program_hash_trace: Array2::default([0, hash_table::BASE_WIDTH]), hash_trace: Array2::default([0, hash_table::BASE_WIDTH]), sponge_trace: Array2::default([0, hash_table::BASE_WIDTH]), @@ -177,6 +180,10 @@ impl AlgebraicExecutionTrace { self.processor_trace.nrows() } + pub fn op_stack_table_length(&self) -> usize { + self.op_stack_underflow_trace.nrows() + } + pub fn hash_table_length(&self) -> usize { self.sponge_trace.nrows() + self.hash_trace.nrows() + self.program_hash_trace.nrows() } @@ -223,7 +230,7 @@ impl AlgebraicExecutionTrace { SpongeStateReset => self.append_initial_sponge_state(), Tip5Trace(instruction, trace) => self.append_sponge_trace(instruction, *trace), U32Call(u32_entry) => self.record_u32_table_entry(u32_entry), - OpStackCall(_) => (), // todo + OpStackCall(op_stack_entry) => self.record_op_stack_entry(op_stack_entry), } } @@ -307,6 +314,13 @@ impl AlgebraicExecutionTrace { fn record_u32_table_entry(&mut self, u32_entry: U32TableEntry) { self.u32_entries.entry(u32_entry).or_insert(0).add_assign(1) } + + fn record_op_stack_entry(&mut self, op_stack_entry: OpStackTableEntry) { + let op_stack_table_row = op_stack_entry.to_base_table_row(); + self.op_stack_underflow_trace + .push_row(op_stack_table_row.view()) + .unwrap(); + } } #[cfg(test)] diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 901f2874e..9d23cd145 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -195,6 +195,13 @@ impl UnderflowIO { Self::Write(_) => true, } } + + pub fn payload(self) -> BFieldElement { + match self { + Self::Read(payload) => payload, + Self::Write(payload) => payload, + } + } } /// Represents the [`OpStack`] registers directly accessible by Triton VM. diff --git a/triton-vm/src/table/master_table.rs b/triton-vm/src/table/master_table.rs index 54bdafea0..7a7d6d239 100644 --- a/triton-vm/src/table/master_table.rs +++ b/triton-vm/src/table/master_table.rs @@ -146,6 +146,8 @@ pub const EXT_DEGREE_LOWERING_TABLE_START: usize = EXT_U32_TABLE_END; pub const EXT_DEGREE_LOWERING_TABLE_END: usize = EXT_DEGREE_LOWERING_TABLE_START + degree_lowering_table::EXT_WIDTH; +const NUM_TABLES_WITHOUT_DEGREE_LOWERING: usize = TableId::COUNT - 1; + /// A `TableId` uniquely determines one of Triton VM's tables. #[derive(Debug, Copy, Clone, Display, EnumCount, EnumIter, PartialEq, Eq, Hash)] pub enum TableId { @@ -327,6 +329,7 @@ pub struct MasterBaseTable { program_table_len: usize, main_execution_len: usize, + op_stack_table_len: usize, hash_coprocessor_execution_len: usize, cascade_table_len: usize, u32_coprocesor_execution_len: usize, @@ -587,6 +590,7 @@ impl MasterBaseTable { num_trace_randomizers, program_table_len: aet.program_table_length(), main_execution_len: aet.processor_table_length(), + op_stack_table_len: aet.op_stack_table_length(), hash_coprocessor_execution_len: aet.hash_table_length(), cascade_table_len: aet.cascade_table_length(), u32_coprocesor_execution_len: aet.u32_table_length(), @@ -705,7 +709,7 @@ impl MasterBaseTable { DegreeLoweringTable::fill_derived_base_columns(self.trace_table_mut()); } - fn all_pad_functions() -> [PadFunction; TableId::COUNT - 1] { + fn all_pad_functions() -> [PadFunction; NUM_TABLES_WITHOUT_DEGREE_LOWERING] { [ ProgramTable::pad_trace, ProcessorTable::pad_trace, @@ -719,13 +723,17 @@ impl MasterBaseTable { ] } - fn all_table_lengths(&self) -> [usize; TableId::COUNT - 1] { + fn all_table_lengths(&self) -> [usize; NUM_TABLES_WITHOUT_DEGREE_LOWERING] { + let processor_table_len = self.main_execution_len; + let ram_table_len = self.main_execution_len; + let jump_stack_table_len = self.main_execution_len; + [ self.program_table_len, + processor_table_len, self.main_execution_len, - self.main_execution_len, - self.main_execution_len, - self.main_execution_len, + ram_table_len, + jump_stack_table_len, self.hash_coprocessor_execution_len, self.cascade_table_len, 1 << 8, @@ -830,7 +838,7 @@ impl MasterBaseTable { master_ext_table } - fn all_extend_functions() -> [ExtendFunction; TableId::COUNT - 1] { + fn all_extend_functions() -> [ExtendFunction; NUM_TABLES_WITHOUT_DEGREE_LOWERING] { [ ProgramTable::extend, ProcessorTable::extend, @@ -844,7 +852,9 @@ impl MasterBaseTable { ] } - fn base_tables_for_extending(&self) -> [ArrayView2; TableId::COUNT - 1] { + fn base_tables_for_extending( + &self, + ) -> [ArrayView2; NUM_TABLES_WITHOUT_DEGREE_LOWERING] { [ self.table(TableId::ProgramTable), self.table(TableId::ProcessorTable), diff --git a/triton-vm/src/table/op_stack_table.rs b/triton-vm/src/table/op_stack_table.rs index fc6fbefa1..191619ce8 100644 --- a/triton-vm/src/table/op_stack_table.rs +++ b/triton-vm/src/table/op_stack_table.rs @@ -8,6 +8,8 @@ use ndarray::ArrayView1; use ndarray::ArrayView2; use ndarray::ArrayViewMut2; use ndarray::Axis; +use num_traits::One; +use num_traits::Zero; use strum::EnumCount; use twenty_first::shared_math::b_field_element::BFieldElement; use twenty_first::shared_math::traits::Inverse; @@ -79,6 +81,20 @@ impl OpStackTableEntry { } op_stack_table_entries } + + pub fn to_base_table_row(self) -> Array1 { + let shrink_stack_indicator = match self.shrinks_stack() { + true => BFieldElement::one(), + false => BFieldElement::zero(), + }; + + let mut row = Array1::zeros(BASE_WIDTH); + row[CLK.base_table_index()] = self.clk.into(); + row[IB1ShrinkStack.base_table_index()] = shrink_stack_indicator; + row[OSP.base_table_index()] = self.op_stack_pointer; + row[OSV.base_table_index()] = self.underflow_io.payload(); + row + } } impl ExtOpStackTable { From a8e696626af0af0e714834cf02b8a40c1e5a9d34 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Fri, 20 Oct 2023 16:53:01 +0200 Subject: [PATCH 12/27] feat: make op stack table variable length --- triton-vm/src/op_stack.rs | 22 +- triton-vm/src/table/challenges.rs | 4 +- triton-vm/src/table/master_table.rs | 2 +- triton-vm/src/table/op_stack_table.rs | 423 +++++++++++++------------ triton-vm/src/table/processor_table.rs | 59 ++-- triton-vm/src/table/table_column.rs | 7 +- triton-vm/src/vm.rs | 7 +- 7 files changed, 252 insertions(+), 272 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 9d23cd145..62344e05d 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -127,7 +127,7 @@ impl OpStack { /// The address of the next free address of the op-stack. Equivalent to the current length of /// the op-stack. - pub(crate) fn op_stack_pointer(&self) -> BFieldElement { + pub(crate) fn pointer(&self) -> BFieldElement { (self.stack.len() as u64).into() } @@ -351,10 +351,7 @@ mod tests { // verify height assert_eq!(op_stack.stack.len(), 16); - assert_eq!( - op_stack.op_stack_pointer().value() as usize, - op_stack.stack.len() - ); + assert_eq!(op_stack.pointer().value() as usize, op_stack.stack.len()); // push elements 1 thru 17 for i in 1..=17 { @@ -363,10 +360,7 @@ mod tests { // verify height assert_eq!(op_stack.stack.len(), 33); - assert_eq!( - op_stack.op_stack_pointer().value() as usize, - op_stack.stack.len() - ); + assert_eq!(op_stack.pointer().value() as usize, op_stack.stack.len()); // verify that all accessible items are different let mut container = vec![ @@ -401,10 +395,7 @@ mod tests { // verify height assert_eq!(op_stack.stack.len(), 22); - assert_eq!( - op_stack.op_stack_pointer().value() as usize, - op_stack.stack.len() - ); + assert_eq!(op_stack.pointer().value() as usize, op_stack.stack.len()); // pop 2 XFieldElements let _ = op_stack.pop_extension_field_element().expect("can't pop"); @@ -412,10 +403,7 @@ mod tests { // verify height assert_eq!(op_stack.stack.len(), 16); - assert_eq!( - op_stack.op_stack_pointer().value() as usize, - op_stack.stack.len() - ); + assert_eq!(op_stack.pointer().value() as usize, op_stack.stack.len()); // verify underflow let _ = op_stack.pop().expect("can't pop"); diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 013eea5d9..6e7b5f81e 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -99,8 +99,8 @@ pub enum ChallengeId { OpStackClkWeight, OpStackIb1Weight, - OpStackOspWeight, - OpStackOsvWeight, + OpStackPointerWeight, + OpStackFirstUnderflowElementWeight, RamClkWeight, RamRampWeight, diff --git a/triton-vm/src/table/master_table.rs b/triton-vm/src/table/master_table.rs index 7a7d6d239..852bd8157 100644 --- a/triton-vm/src/table/master_table.rs +++ b/triton-vm/src/table/master_table.rs @@ -731,7 +731,7 @@ impl MasterBaseTable { [ self.program_table_len, processor_table_len, - self.main_execution_len, + self.op_stack_table_len, ram_table_len, jump_stack_table_len, self.hash_coprocessor_execution_len, diff --git a/triton-vm/src/table/op_stack_table.rs b/triton-vm/src/table/op_stack_table.rs index 191619ce8..56ba83446 100644 --- a/triton-vm/src/table/op_stack_table.rs +++ b/triton-vm/src/table/op_stack_table.rs @@ -1,6 +1,7 @@ use std::cmp::Ordering; use arbitrary::Arbitrary; +use itertools::Itertools; use ndarray::parallel::prelude::*; use ndarray::s; use ndarray::Array1; @@ -32,6 +33,10 @@ pub const BASE_WIDTH: usize = OpStackBaseTableColumn::COUNT; pub const EXT_WIDTH: usize = OpStackExtTableColumn::COUNT; pub const FULL_WIDTH: usize = BASE_WIDTH + EXT_WIDTH; +/// The value indicating a padding row in the op stack table. Stored in the `ib1_shrink_stack` +/// column. +pub(crate) const PADDING_VALUE: BFieldElement = BFieldElement::new(2); + #[derive(Debug, Clone)] pub struct OpStackTable {} @@ -91,8 +96,8 @@ impl OpStackTableEntry { let mut row = Array1::zeros(BASE_WIDTH); row[CLK.base_table_index()] = self.clk.into(); row[IB1ShrinkStack.base_table_index()] = shrink_stack_indicator; - row[OSP.base_table_index()] = self.op_stack_pointer; - row[OSV.base_table_index()] = self.underflow_io.payload(); + row[StackPointer.base_table_index()] = self.op_stack_pointer; + row[FirstUnderflowElement.base_table_index()] = self.underflow_io.payload(); row } } @@ -101,35 +106,45 @@ impl ExtOpStackTable { pub fn initial_constraints( circuit_builder: &ConstraintCircuitBuilder, ) -> Vec> { - let clk = circuit_builder.input(BaseRow(CLK.master_base_table_index())); - let ib1 = circuit_builder.input(BaseRow(IB1ShrinkStack.master_base_table_index())); - let osp = circuit_builder.input(BaseRow(OSP.master_base_table_index())); - let osv = circuit_builder.input(BaseRow(OSV.master_base_table_index())); - let rppa = circuit_builder.input(ExtRow(RunningProductPermArg.master_ext_table_index())); - let clock_jump_diff_log_derivative = circuit_builder.input(ExtRow( - ClockJumpDifferenceLookupClientLogDerivative.master_ext_table_index(), - )); - - let clk_is_0 = clk; - let osv_is_0 = osv; - let osp_is_16 = osp - circuit_builder.b_constant(16_u32.into()); - - // The running product for the permutation argument `rppa` starts off having accumulated the - // first row. Note that `clk` and `osv` are constrained to be 0, and `osp` to be 16. - let compressed_row = circuit_builder.challenge(OpStackIb1Weight) * ib1 - + circuit_builder.challenge(OpStackOspWeight) - * circuit_builder.b_constant(16_u32.into()); - let processor_perm_indeterminate = circuit_builder.challenge(OpStackIndeterminate); - let rppa_initial = processor_perm_indeterminate - compressed_row; - let rppa_starts_correctly = rppa - rppa_initial; - - let clock_jump_diff_log_derivative_is_initialized_correctly = clock_jump_diff_log_derivative - - circuit_builder.x_constant(LookupArg::default_initial()); + let challenge = |c| circuit_builder.challenge(c); + let constant = |c| circuit_builder.b_constant(c); + let x_constant = |c| circuit_builder.x_constant(c); + let base_row = |column: OpStackBaseTableColumn| { + circuit_builder.input(BaseRow(column.master_base_table_index())) + }; + let ext_row = |column: OpStackExtTableColumn| { + circuit_builder.input(ExtRow(column.master_ext_table_index())) + }; + + let initial_stack_length = u32::try_from(OpStackElement::COUNT).unwrap(); + let initial_stack_length = constant(initial_stack_length.into()); + let padding_indicator = constant(PADDING_VALUE); + + let stack_pointer_is_16 = base_row(StackPointer) - initial_stack_length.clone(); + + let compressed_row = challenge(OpStackClkWeight) * base_row(CLK) + + challenge(OpStackIb1Weight) * base_row(IB1ShrinkStack) + + challenge(OpStackPointerWeight) * initial_stack_length + + challenge(OpStackFirstUnderflowElementWeight) * base_row(FirstUnderflowElement); + let rppa_initial = challenge(OpStackIndeterminate) - compressed_row; + let rppa_has_accumulated_first_row = ext_row(RunningProductPermArg) - rppa_initial; + + let rppa_is_default_initial = + ext_row(RunningProductPermArg) - x_constant(PermArg::default_initial()); + + let first_row_is_padding_row = base_row(IB1ShrinkStack) - padding_indicator; + let first_row_is_not_padding_row = + base_row(IB1ShrinkStack) * (base_row(IB1ShrinkStack) - constant(1_u64.into())); + + let rppa_starts_correctly = rppa_has_accumulated_first_row * first_row_is_padding_row + + rppa_is_default_initial * first_row_is_not_padding_row; + + let lookup_argument_initial = x_constant(LookupArg::default_initial()); + let clock_jump_diff_log_derivative_is_initialized_correctly = + ext_row(ClockJumpDifferenceLookupClientLogDerivative) - lookup_argument_initial; vec![ - clk_is_0, - osv_is_0, - osp_is_16, + stack_pointer_is_16, rppa_starts_correctly, clock_jump_diff_log_derivative_is_initialized_correctly, ] @@ -145,73 +160,97 @@ impl ExtOpStackTable { pub fn transition_constraints( circuit_builder: &ConstraintCircuitBuilder, ) -> Vec> { - let one = circuit_builder.b_constant(1u32.into()); - - let clk = circuit_builder.input(CurrentBaseRow(CLK.master_base_table_index())); - let ib1_shrink_stack = - circuit_builder.input(CurrentBaseRow(IB1ShrinkStack.master_base_table_index())); - let osp = circuit_builder.input(CurrentBaseRow(OSP.master_base_table_index())); - let osv = circuit_builder.input(CurrentBaseRow(OSV.master_base_table_index())); - let rppa = circuit_builder.input(CurrentExtRow( - RunningProductPermArg.master_ext_table_index(), - )); - let clock_jump_diff_log_derivative = circuit_builder.input(CurrentExtRow( - ClockJumpDifferenceLookupClientLogDerivative.master_ext_table_index(), - )); - - let clk_next = circuit_builder.input(NextBaseRow(CLK.master_base_table_index())); - let ib1_shrink_stack_next = - circuit_builder.input(NextBaseRow(IB1ShrinkStack.master_base_table_index())); - let osp_next = circuit_builder.input(NextBaseRow(OSP.master_base_table_index())); - let osv_next = circuit_builder.input(NextBaseRow(OSV.master_base_table_index())); - let rppa_next = - circuit_builder.input(NextExtRow(RunningProductPermArg.master_ext_table_index())); - let clock_jump_diff_log_derivative_next = circuit_builder.input(NextExtRow( - ClockJumpDifferenceLookupClientLogDerivative.master_ext_table_index(), - )); - - // the osp increases by 1 or the osp does not change - // - // $(osp' - (osp + 1))·(osp' - osp) = 0$ - let osp_increases_by_1_or_does_not_change = - (osp_next.clone() - osp.clone() - one.clone()) * (osp_next.clone() - osp.clone()); - - // the osp increases by 1 or the osv does not change OR the ci shrinks the OpStack - // - // $ (osp' - (osp + 1)) · (osv' - osv) · (1 - ib1) = 0$ - let osp_increases_by_1_or_osv_does_not_change_or_shrink_stack = - (osp_next.clone() - osp.clone() - one.clone()) - * (osv_next.clone() - osv) - * (one.clone() - ib1_shrink_stack); + let constant = |c| circuit_builder.b_constant(c); + let challenge = |c| circuit_builder.challenge(c); + let current_base_row = |column: OpStackBaseTableColumn| { + circuit_builder.input(CurrentBaseRow(column.master_base_table_index())) + }; + let current_ext_row = |column: OpStackExtTableColumn| { + circuit_builder.input(CurrentExtRow(column.master_ext_table_index())) + }; + let next_base_row = |column: OpStackBaseTableColumn| { + circuit_builder.input(NextBaseRow(column.master_base_table_index())) + }; + let next_ext_row = |column: OpStackExtTableColumn| { + circuit_builder.input(NextExtRow(column.master_ext_table_index())) + }; + + let one = constant(1_u32.into()); + let padding_indicator = constant(PADDING_VALUE); + + let clk = current_base_row(CLK); + let ib1_shrink_stack = current_base_row(IB1ShrinkStack); + let stack_pointer = current_base_row(StackPointer); + let first_underflow_element = current_base_row(FirstUnderflowElement); + let rppa = current_ext_row(RunningProductPermArg); + let clock_jump_diff_log_derivative = + current_ext_row(ClockJumpDifferenceLookupClientLogDerivative); + + let clk_next = next_base_row(CLK); + let ib1_shrink_stack_next = next_base_row(IB1ShrinkStack); + let stack_pointer_next = next_base_row(StackPointer); + let first_underflow_element_next = next_base_row(FirstUnderflowElement); + let rppa_next = next_ext_row(RunningProductPermArg); + let clock_jump_diff_log_derivative_next = + next_ext_row(ClockJumpDifferenceLookupClientLogDerivative); + + let stack_pointer_increases_by_1_or_does_not_change = + (stack_pointer_next.clone() - stack_pointer.clone() - one.clone()) + * (stack_pointer_next.clone() - stack_pointer.clone()); + + let stack_pointer_inc_by_1_or_underflow_element_doesnt_change_or_next_ci_grows_stack = + (stack_pointer_next.clone() - stack_pointer.clone() - one.clone()) + * (first_underflow_element_next.clone() - first_underflow_element.clone()) + * ib1_shrink_stack_next.clone(); + + let next_row_is_padding_row = ib1_shrink_stack_next.clone() - padding_indicator.clone(); + let if_current_row_is_padding_row_then_next_row_is_padding_row = ib1_shrink_stack.clone() + * (ib1_shrink_stack - one.clone()) + * next_row_is_padding_row.clone(); // The running product for the permutation argument `rppa` is updated correctly. - let alpha = circuit_builder.challenge(OpStackIndeterminate); let compressed_row = circuit_builder.challenge(OpStackClkWeight) * clk_next.clone() - + circuit_builder.challenge(OpStackIb1Weight) * ib1_shrink_stack_next - + circuit_builder.challenge(OpStackOspWeight) * osp_next.clone() - + circuit_builder.challenge(OpStackOsvWeight) * osv_next; + + circuit_builder.challenge(OpStackIb1Weight) * ib1_shrink_stack_next.clone() + + circuit_builder.challenge(OpStackPointerWeight) * stack_pointer_next.clone() + + circuit_builder.challenge(OpStackFirstUnderflowElementWeight) + * first_underflow_element_next; - let rppa_updates_correctly = rppa_next - rppa * (alpha - compressed_row); + let rppa_updates = + rppa_next.clone() - rppa.clone() * (challenge(OpStackIndeterminate) - compressed_row); + + let next_row_is_not_padding_row = + ib1_shrink_stack_next.clone() * (ib1_shrink_stack_next.clone() - one.clone()); + let rppa_remains = rppa_next - rppa; + + let rppa_updates_correctly = rppa_updates * next_row_is_padding_row.clone() + + rppa_remains * next_row_is_not_padding_row.clone(); - // The running sum of the logarithmic derivative for the clock jump difference Lookup - // Argument accumulates a summand of `clk_diff` if and only if the `osp` does not change. - // Expressed differently: - // - the `osp` changes or the log derivative accumulates a summand, and - // - the `osp` does not change or the log derivative does not change. - let log_derivative_remains = - clock_jump_diff_log_derivative_next.clone() - clock_jump_diff_log_derivative.clone(); let clk_diff = clk_next - clk; - let log_derivative_accumulates = (clock_jump_diff_log_derivative_next - - clock_jump_diff_log_derivative) - * (circuit_builder.challenge(ClockJumpDifferenceLookupIndeterminate) - clk_diff) + let log_derivative_accumulates = (clock_jump_diff_log_derivative_next.clone() + - clock_jump_diff_log_derivative.clone()) + * (challenge(ClockJumpDifferenceLookupIndeterminate) - clk_diff) - one.clone(); - let log_derivative_updates_correctly = (osp_next.clone() - osp.clone() - one) - * log_derivative_accumulates - + (osp_next - osp) * log_derivative_remains; + let log_derivative_remains = + clock_jump_diff_log_derivative_next.clone() - clock_jump_diff_log_derivative.clone(); + + let log_derivative_accumulates_or_stack_pointer_changes_or_next_row_is_padding_row = + log_derivative_accumulates + * (stack_pointer_next.clone() - stack_pointer.clone() - one.clone()) + * next_row_is_padding_row; + let log_derivative_remains_or_stack_pointer_doesnt_change = + log_derivative_remains.clone() * (stack_pointer_next.clone() - stack_pointer.clone()); + let log_derivatve_remains_or_next_row_is_not_padding_row = + log_derivative_remains.clone() * next_row_is_not_padding_row; + + let log_derivative_updates_correctly = + log_derivative_accumulates_or_stack_pointer_changes_or_next_row_is_padding_row + + log_derivative_remains_or_stack_pointer_doesnt_change + + log_derivatve_remains_or_next_row_is_not_padding_row; vec![ - osp_increases_by_1_or_does_not_change, - osp_increases_by_1_or_osv_does_not_change_or_shrink_stack, + stack_pointer_increases_by_1_or_does_not_change, + stack_pointer_inc_by_1_or_underflow_element_doesnt_change_or_next_ci_grows_stack, + if_current_row_is_padding_row_then_next_row_is_padding_row, rppa_updates_correctly, log_derivative_updates_correctly, ] @@ -231,111 +270,64 @@ impl OpStackTable { op_stack_table: &mut ArrayViewMut2, aet: &AlgebraicExecutionTrace, ) -> Vec { - // Store the registers relevant for the Op Stack Table, i.e., CLK, IB1, OSP, and OSV, - // with OSP as the key. Preserves, thus allows reusing, the order of the processor's - // rows, which are sorted by CLK. - let mut pre_processed_op_stack_table: Vec> = vec![]; - for processor_row in aet.processor_trace.rows() { - let clk = processor_row[ProcessorBaseTableColumn::CLK.base_table_index()]; - let ib1 = processor_row[ProcessorBaseTableColumn::IB1.base_table_index()]; - let osp = processor_row[ProcessorBaseTableColumn::OSP.base_table_index()]; - let osv = processor_row[ProcessorBaseTableColumn::OSV.base_table_index()]; - // The (honest) prover can only grow the Op Stack's size by at most 1 per execution - // step. Hence, the following (a) works, and (b) sorts. - let osp_minus_16 = osp.value() as usize - OpStackElement::COUNT; - let op_stack_row = (clk, ib1, osv); - match osp_minus_16.cmp(&pre_processed_op_stack_table.len()) { - Ordering::Less => pre_processed_op_stack_table[osp_minus_16].push(op_stack_row), - Ordering::Equal => pre_processed_op_stack_table.push(vec![op_stack_row]), - Ordering::Greater => panic!("OSP must increase by at most 1 per execution step."), - } - } + let mut op_stack_table = op_stack_table.slice_mut(s![0..aet.op_stack_table_length(), ..]); + let trace_iter = aet.op_stack_underflow_trace.rows().into_iter(); - // Move the rows into the Op Stack Table, sorted by OSP first, CLK second. - let mut op_stack_table_row = 0; - for (osp_minus_16, rows_with_this_osp) in - pre_processed_op_stack_table.into_iter().enumerate() - { - let osp = BFieldElement::new((osp_minus_16 + OpStackElement::COUNT) as u64); - for (clk, ib1, osv) in rows_with_this_osp { - op_stack_table[[op_stack_table_row, CLK.base_table_index()]] = clk; - op_stack_table[[op_stack_table_row, IB1ShrinkStack.base_table_index()]] = ib1; - op_stack_table[[op_stack_table_row, OSP.base_table_index()]] = osp; - op_stack_table[[op_stack_table_row, OSV.base_table_index()]] = osv; - op_stack_table_row += 1; - } + let sorted_rows = + trace_iter.sorted_by(|row_0, row_1| Self::compare_rows(row_0.view(), row_1.view())); + for (row_index, row) in sorted_rows.enumerate() { + op_stack_table.row_mut(row_index).assign(&row); } - assert_eq!(aet.processor_trace.nrows(), op_stack_table_row); - // Collect all clock jump differences. - // The Op Stack Table and the Processor Table have the same length. + Self::clock_jump_differences(op_stack_table.view()) + } + + fn compare_rows( + row_0: ArrayView1, + row_1: ArrayView1, + ) -> Ordering { + let stack_pointer_0 = row_0[StackPointer.base_table_index()].value(); + let stack_pointer_1 = row_1[StackPointer.base_table_index()].value(); + let compare_stack_pointers = stack_pointer_0.cmp(&stack_pointer_1); + + let clk_0 = row_0[CLK.base_table_index()].value(); + let clk_1 = row_1[CLK.base_table_index()].value(); + let compare_clocks = clk_0.cmp(&clk_1); + + compare_stack_pointers.then(compare_clocks) + } + + fn clock_jump_differences(op_stack_table: ArrayView2) -> Vec { let mut clock_jump_differences = vec![]; - for row_idx in 0..aet.processor_trace.nrows() - 1 { - let curr_row = op_stack_table.row(row_idx); - let next_row = op_stack_table.row(row_idx + 1); - let clk_diff = next_row[CLK.base_table_index()] - curr_row[CLK.base_table_index()]; - if curr_row[OSP.base_table_index()] == next_row[OSP.base_table_index()] { - clock_jump_differences.push(clk_diff); + for consecutive_rows in op_stack_table.axis_windows(Axis(0), 2).into_iter() { + let current_row = consecutive_rows.row(0); + let next_row = consecutive_rows.row(1); + let current_stack_pointer = current_row[StackPointer.base_table_index()]; + let next_stack_pointer = next_row[StackPointer.base_table_index()]; + if current_stack_pointer == next_stack_pointer { + let current_clk = current_row[CLK.base_table_index()]; + let next_clk = next_row[CLK.base_table_index()]; + let clk_difference = next_clk - current_clk; + clock_jump_differences.push(clk_difference); } } clock_jump_differences } - pub fn pad_trace(mut op_stack_table: ArrayViewMut2, processor_table_len: usize) { - assert!( - processor_table_len > 0, - "Processor Table must have at least 1 row." - ); - - // Set up indices for relevant sections of the table. - let padded_height = op_stack_table.nrows(); - let num_padding_rows = padded_height - processor_table_len; - let max_clk_before_padding = processor_table_len - 1; - let max_clk_before_padding_row_idx = op_stack_table - .rows() - .into_iter() - .enumerate() - .find(|(_, row)| row[CLK.base_table_index()].value() as usize == max_clk_before_padding) - .map(|(idx, _)| idx) - .expect("Op Stack Table must contain row with clock cycle equal to max cycle."); - let rows_to_move_source_section_start = max_clk_before_padding_row_idx + 1; - let rows_to_move_source_section_end = processor_table_len; - let num_rows_to_move = rows_to_move_source_section_end - rows_to_move_source_section_start; - let rows_to_move_dest_section_start = rows_to_move_source_section_start + num_padding_rows; - let rows_to_move_dest_section_end = rows_to_move_dest_section_start + num_rows_to_move; - let padding_section_start = rows_to_move_source_section_start; - let padding_section_end = padding_section_start + num_padding_rows; - assert_eq!(padded_height, rows_to_move_dest_section_end); - - // Move all rows below the row with highest CLK to the end of the table – if they exist. - if num_rows_to_move > 0 { - let rows_to_move_source_range = - rows_to_move_source_section_start..rows_to_move_source_section_end; - let rows_to_move_dest_range = - rows_to_move_dest_section_start..rows_to_move_dest_section_end; - let rows_to_move = op_stack_table - .slice(s![rows_to_move_source_range, ..]) - .to_owned(); - rows_to_move.move_into(&mut op_stack_table.slice_mut(s![rows_to_move_dest_range, ..])); + pub fn pad_trace(mut op_stack_table: ArrayViewMut2, op_stack_table_len: usize) { + let last_row_index = op_stack_table_len.saturating_sub(1); + let mut last_row = op_stack_table.row(last_row_index).to_owned(); + last_row[IB1ShrinkStack.base_table_index()] = PADDING_VALUE; + if op_stack_table_len == 0 { + let first_stack_pointer = u32::try_from(OpStackElement::COUNT).unwrap().into(); + last_row[StackPointer.base_table_index()] = first_stack_pointer; } - // Fill the created gap with padding rows, i.e., with copies of the last row before the - // gap. This is the padding section. - let padding_row_template = op_stack_table - .row(max_clk_before_padding_row_idx) - .to_owned(); - let mut padding_section = - op_stack_table.slice_mut(s![padding_section_start..padding_section_end, ..]); + let mut padding_section = op_stack_table.slice_mut(s![op_stack_table_len.., ..]); padding_section .axis_iter_mut(Axis(0)) .into_par_iter() - .for_each(|padding_row| padding_row_template.clone().move_into(padding_row)); - - // CLK keeps increasing by 1 also in the padding section. - let new_clk_values = Array1::from_iter( - (processor_table_len..padded_height).map(|clk| BFieldElement::new(clk as u64)), - ); - new_clk_values.move_into(padding_section.slice_mut(s![.., CLK.base_table_index()])); + .for_each(|mut row| row.assign(&last_row)); } pub fn extend( @@ -349,8 +341,8 @@ impl OpStackTable { let clk_weight = challenges[OpStackClkWeight]; let ib1_weight = challenges[OpStackIb1Weight]; - let osp_weight = challenges[OpStackOspWeight]; - let osv_weight = challenges[OpStackOsvWeight]; + let stack_pointer_weight = challenges[OpStackPointerWeight]; + let first_underflow_element_weight = challenges[OpStackFirstUnderflowElementWeight]; let perm_arg_indeterminate = challenges[OpStackIndeterminate]; let clock_jump_difference_lookup_indeterminate = challenges[ClockJumpDifferenceLookupIndeterminate]; @@ -363,21 +355,30 @@ impl OpStackTable { let current_row = base_table.row(row_idx); let clk = current_row[CLK.base_table_index()]; let ib1 = current_row[IB1ShrinkStack.base_table_index()]; - let osp = current_row[OSP.base_table_index()]; - let osv = current_row[OSV.base_table_index()]; - - let compressed_row_for_permutation_argument = - clk * clk_weight + ib1 * ib1_weight + osp * osp_weight + osv * osv_weight; - running_product *= perm_arg_indeterminate - compressed_row_for_permutation_argument; - - // clock jump difference - if let Some(prev_row) = previous_row { - if prev_row[OSP.base_table_index()] == current_row[OSP.base_table_index()] { - let clock_jump_difference = - current_row[CLK.base_table_index()] - prev_row[CLK.base_table_index()]; - clock_jump_diff_lookup_log_derivative += - (clock_jump_difference_lookup_indeterminate - clock_jump_difference) - .inverse(); + let stack_pointer = current_row[StackPointer.base_table_index()]; + let first_underflow_element = current_row[FirstUnderflowElement.base_table_index()]; + + let is_no_padding_row = ib1 != PADDING_VALUE; + + if is_no_padding_row { + let compressed_row = clk * clk_weight + + ib1 * ib1_weight + + stack_pointer * stack_pointer_weight + + first_underflow_element * first_underflow_element_weight; + running_product *= perm_arg_indeterminate - compressed_row; + + // clock jump difference + if let Some(prev_row) = previous_row { + let previous_stack_pointer = prev_row[StackPointer.base_table_index()]; + let current_stack_pointer = current_row[StackPointer.base_table_index()]; + if previous_stack_pointer == current_stack_pointer { + let previous_clock = prev_row[CLK.base_table_index()]; + let current_clock = current_row[CLK.base_table_index()]; + let clock_jump_difference = current_clock - previous_clock; + let log_derivative_summand = + clock_jump_difference_lookup_indeterminate - clock_jump_difference; + clock_jump_diff_lookup_log_derivative += log_derivative_summand.inverse(); + } } } @@ -400,6 +401,8 @@ pub(crate) mod tests { use proptest::prelude::*; use proptest_arbitrary_interop::arb; + use crate::op_stack::OpStackElement; + use super::*; pub fn constraints_evaluate_to_zero( @@ -505,18 +508,18 @@ pub(crate) mod tests { #[test] fn op_stack_pointer_in_sequence_of_op_stack_table_entries( clk: u32, - osp in OpStackElement::COUNT..1024, + stack_pointer in OpStackElement::COUNT..1024, base_field_elements in vec(arb::(), 0..32), sequence_of_writes: bool, ) { let sequence_length = base_field_elements.len(); - let osp = match sequence_of_writes { - true => osp, - false => max(osp, sequence_length), + let stack_pointer = match sequence_of_writes { + true => stack_pointer, + false => max(stack_pointer, sequence_length), }; let sequence_length = u64::try_from(sequence_length).unwrap(); - let osp = u64::try_from(osp).unwrap(); + let stack_pointer = u64::try_from(stack_pointer).unwrap(); let underflow_io_operation = match sequence_of_writes { true => UnderflowIO::Write, @@ -527,7 +530,7 @@ pub(crate) mod tests { .map(underflow_io_operation) .collect(); - let op_stack_pointer = osp.into(); + let op_stack_pointer = stack_pointer.into(); let entries = OpStackTableEntry::from_underflow_io_sequence(clk, op_stack_pointer, underflow_io); let op_stack_pointers = entries @@ -536,16 +539,16 @@ pub(crate) mod tests { .sorted() .collect_vec(); - let expected_lowest_osp = match sequence_of_writes { - true => osp, - false => osp - sequence_length, + let expected_lowest_stack_pointer = match sequence_of_writes { + true => stack_pointer, + false => stack_pointer - sequence_length, }; - let expected_largest_osp = match sequence_of_writes { - true => osp + sequence_length, - false => osp, + let expected_largest_stack_pointer = match sequence_of_writes { + true => stack_pointer + sequence_length, + false => stack_pointer, }; let expected_op_stack_pointers = - (expected_lowest_osp..expected_largest_osp).collect_vec(); + (expected_lowest_stack_pointer..expected_largest_stack_pointer).collect_vec(); prop_assert_eq!(expected_op_stack_pointers, op_stack_pointers); } @@ -555,10 +558,10 @@ pub(crate) mod tests { #[test] fn clk_stays_same_in_sequence_of_op_stack_table_entries( clk: u32, - osp in OpStackElement::COUNT..1024, + stack_pointer in OpStackElement::COUNT..1024, underflow_io in vec(arb::(), 0..OpStackElement::COUNT), ) { - let op_stack_pointer = u64::try_from(osp).unwrap().into(); + let op_stack_pointer = u64::try_from(stack_pointer).unwrap().into(); let entries = OpStackTableEntry::from_underflow_io_sequence(clk, op_stack_pointer, underflow_io); let clk_values = entries.iter().map(|entry| entry.clk).collect_vec(); diff --git a/triton-vm/src/table/processor_table.rs b/triton-vm/src/table/processor_table.rs index 3b9e13b64..74b1bdf53 100644 --- a/triton-vm/src/table/processor_table.rs +++ b/triton-vm/src/table/processor_table.rs @@ -181,13 +181,13 @@ impl ProcessorTable { // OpStack table let clk = current_row[CLK.base_table_index()]; let ib1 = current_row[IB1.base_table_index()]; - let osp = current_row[OSP.base_table_index()]; - let osv = current_row[OSV.base_table_index()]; + let osp = current_row[OpStackPointer.base_table_index()]; + let osv = current_row[ST15.base_table_index()]; // todo let compressed_row_for_op_stack_table_permutation_argument = clk * challenges[OpStackClkWeight] + ib1 * challenges[OpStackIb1Weight] - + osp * challenges[OpStackOspWeight] - + osv * challenges[OpStackOsvWeight]; + + osp * challenges[OpStackPointerWeight] + + osv * challenges[OpStackFirstUnderflowElementWeight]; op_stack_table_running_product *= challenges[OpStackIndeterminate] - compressed_row_for_op_stack_table_permutation_argument; @@ -392,8 +392,7 @@ impl ExtProcessorTable { let st8_is_0 = base_row(ST8); let st9_is_0 = base_row(ST9); let st10_is_0 = base_row(ST10); - let osp_is_16 = base_row(OSP) - constant(16); - let osv_is_0 = base_row(OSV); + let op_stack_pointer_is_16 = base_row(OpStackPointer) - constant(16); let ramp_is_0 = base_row(RAMP); let previous_instruction_is_0 = base_row(PreviousInstruction); @@ -440,10 +439,11 @@ impl ExtProcessorTable { // op-stack table let op_stack_indeterminate = challenge(OpStackIndeterminate); let op_stack_ib1_weight = challenge(OpStackIb1Weight); - let op_stack_osp_weight = challenge(OpStackOspWeight); - // note: `clk` and `osv` are already constrained to be 0, `osp` to be 16 + let op_stack_pointer_weight = challenge(OpStackPointerWeight); + // note: `clk` and `first_underflow_element` are already constrained to be 0, and + // `op_stack_pointer` to be 16 let compressed_row_for_op_stack_table = - op_stack_ib1_weight * base_row(IB1) + op_stack_osp_weight * constant(16); + op_stack_ib1_weight * base_row(IB1) + op_stack_pointer_weight * constant(16); let running_product_for_op_stack_table_is_initialized_correctly = ext_row(OpStackTablePermArg) - x_constant(PermArg::default_initial()) @@ -520,8 +520,7 @@ impl ExtProcessorTable { st9_is_0, st10_is_0, compressed_program_digest_is_expected_program_digest, - osp_is_16, - osv_is_0, + op_stack_pointer_is_16, ramp_is_0, previous_instruction_is_0, running_evaluation_for_standard_input_is_initialized_correctly, @@ -800,10 +799,8 @@ impl ExtProcessorTable { next_base_row(ST13) - curr_base_row(ST13), next_base_row(ST14) - curr_base_row(ST14), next_base_row(ST15) - curr_base_row(ST15), - // The top of the OpStack underflow, i.e., osv, does not change. - next_base_row(OSV) - curr_base_row(OSV), - // The OpStack pointer, osp, does not change. - next_base_row(OSP) - curr_base_row(OSP), + // The OpStack pointer does not change. + next_base_row(OpStackPointer) - curr_base_row(OpStackPointer), ] } @@ -919,10 +916,8 @@ impl ExtProcessorTable { next_base_row(ST13) - curr_base_row(ST12), next_base_row(ST14) - curr_base_row(ST13), next_base_row(ST15) - curr_base_row(ST14), - // The stack element in st15 is moved to the top of OpStack underflow, i.e., osv. - next_base_row(OSV) - curr_base_row(ST15), // The OpStack pointer is incremented by 1. - next_base_row(OSP) - (curr_base_row(OSP) + constant(1)), + next_base_row(OpStackPointer) - (curr_base_row(OpStackPointer) + constant(1)), ] } @@ -972,12 +967,10 @@ impl ExtProcessorTable { next_base_row(ST12) - curr_base_row(ST13), next_base_row(ST13) - curr_base_row(ST14), next_base_row(ST14) - curr_base_row(ST15), - // The stack element at the top of OpStack underflow, i.e., osv, is moved into st15. - next_base_row(ST15) - curr_base_row(OSV), - // The OpStack pointer, osp, is decremented by 1. - next_base_row(OSP) - (curr_base_row(OSP) - constant(1)), - // The helper variable register hv0 holds the inverse of (osp - 16). - (curr_base_row(OSP) - constant(16)) * curr_base_row(HV0) - constant(1), + // The OpStack pointer, is decremented by 1. + next_base_row(OpStackPointer) - (curr_base_row(OpStackPointer) - constant(1)), + // The helper variable register hv0 holds the inverse of (`op_stack_pointer` - 16). + (curr_base_row(OpStackPointer) - constant(16)) * curr_base_row(HV0) - constant(1), ] } @@ -1333,8 +1326,7 @@ impl ExtProcessorTable { (one() - indicator_poly(13)) * (next_base_row(ST13) - curr_base_row(ST13)), (one() - indicator_poly(14)) * (next_base_row(ST14) - curr_base_row(ST14)), (one() - indicator_poly(15)) * (next_base_row(ST15) - curr_base_row(ST15)), - next_base_row(OSV) - curr_base_row(OSV), - next_base_row(OSP) - curr_base_row(OSP), + next_base_row(OpStackPointer) - curr_base_row(OpStackPointer), ]; [ specific_constraints, @@ -2347,8 +2339,8 @@ impl ExtProcessorTable { let compressed_row = challenge(OpStackClkWeight) * next_base_row(CLK) + challenge(OpStackIb1Weight) * next_base_row(IB1) - + challenge(OpStackOspWeight) * next_base_row(OSP) - + challenge(OpStackOsvWeight) * next_base_row(OSV); + + challenge(OpStackPointerWeight) * next_base_row(OpStackPointer) + + challenge(OpStackFirstUnderflowElementWeight) * next_base_row(ST15); // todo!!! next_ext_row(OpStackTablePermArg) - curr_ext_row(OpStackTablePermArg) * (challenge(OpStackIndeterminate) - compressed_row) @@ -2842,7 +2834,7 @@ impl<'a> Display for ProcessorTraceRow<'a> { row( f, format!( - "ramp: {:>width$} │ ramv: {:>width$} │", + "ramp: {:>width$} │ ramv: {:>width$} ╵", self.row[RAMP.base_table_index()].value(), self.row[RAMV.base_table_index()].value(), ), @@ -2850,9 +2842,8 @@ impl<'a> Display for ProcessorTraceRow<'a> { row( f, format!( - "osp: {:>width$} │ osv: {:>width$} ╵", - self.row[OSP.base_table_index()].value(), - self.row[OSV.base_table_index()].value(), + "osp: {:>width$} ╵", + self.row[OpStackPointer.base_table_index()].value(), ), )?; @@ -3533,8 +3524,8 @@ pub(crate) mod tests { test_constraints_for_rows_with_debug_info( XbMul, &test_rows, - &[ST0, ST1, ST2, ST3, OSP, HV0], - &[ST0, ST1, ST2, ST3, OSP, HV0], + &[ST0, ST1, ST2, ST3, OpStackPointer, HV0], + &[ST0, ST1, ST2, ST3, OpStackPointer, HV0], ); } diff --git a/triton-vm/src/table/table_column.rs b/triton-vm/src/table/table_column.rs index f2dcddff3..884fb98d2 100644 --- a/triton-vm/src/table/table_column.rs +++ b/triton-vm/src/table/table_column.rs @@ -135,8 +135,7 @@ pub enum ProcessorBaseTableColumn { ST13, ST14, ST15, - OSP, - OSV, + OpStackPointer, HV0, HV1, HV2, @@ -183,8 +182,8 @@ pub enum ProcessorExtTableColumn { pub enum OpStackBaseTableColumn { CLK, IB1ShrinkStack, - OSP, - OSV, + StackPointer, + FirstUnderflowElement, } #[repr(usize)] diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index eea8f529d..977575d3a 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -143,7 +143,7 @@ impl<'pgm> VMState<'pgm> { }; if current_instruction.shrinks_op_stack() { - let op_stack_pointer = self.op_stack.op_stack_pointer(); + let op_stack_pointer = self.op_stack.pointer(); let maximum_op_stack_pointer = BFieldElement::new(OpStackElement::COUNT as u64); let op_stack_pointer_minus_maximum = op_stack_pointer - maximum_op_stack_pointer; hvs[0] = op_stack_pointer_minus_maximum.inverse_or_zero(); @@ -266,7 +266,7 @@ impl<'pgm> VMState<'pgm> { ) -> Vec { let op_stack_table_entries = OpStackTableEntry::from_underflow_io_sequence( self.cycle_count, - self.op_stack.op_stack_pointer(), + self.op_stack.pointer(), underflow_io_sequence, ); op_stack_table_entries @@ -830,8 +830,7 @@ impl<'pgm> VMState<'pgm> { processor_row[ST13.base_table_index()] = self.op_stack.peek_at(OpStackElement::ST13); processor_row[ST14.base_table_index()] = self.op_stack.peek_at(OpStackElement::ST14); processor_row[ST15.base_table_index()] = self.op_stack.peek_at(OpStackElement::ST15); - processor_row[OSP.base_table_index()] = self.op_stack.op_stack_pointer(); - processor_row[OSV.base_table_index()] = self.op_stack.first_underflow_element(); + processor_row[OpStackPointer.base_table_index()] = self.op_stack.pointer(); processor_row[HV0.base_table_index()] = helper_variables[0]; processor_row[HV1.base_table_index()] = helper_variables[1]; processor_row[HV2.base_table_index()] = helper_variables[2]; From 2c2ade0bdc723c22b3fdeb753014ceb77c6682c6 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 12:25:55 +0200 Subject: [PATCH 13/27] fix: processor's running product with op stack Only update the running product when the op stack's size changes. --- triton-vm/src/table/processor_table.rs | 202 ++++++++++++++++++------- 1 file changed, 145 insertions(+), 57 deletions(-) diff --git a/triton-vm/src/table/processor_table.rs b/triton-vm/src/table/processor_table.rs index 74b1bdf53..a3c0871aa 100644 --- a/triton-vm/src/table/processor_table.rs +++ b/triton-vm/src/table/processor_table.rs @@ -10,8 +10,7 @@ use ndarray::*; use num_traits::One; 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::*; use twenty_first::shared_math::digest::DIGEST_LENGTH; use twenty_first::shared_math::traits::Inverse; use twenty_first::shared_math::x_field_element::XFieldElement; @@ -21,6 +20,7 @@ use crate::instruction::AnInstruction::*; use crate::instruction::Instruction; use crate::instruction::InstructionBit; use crate::instruction::ALL_INSTRUCTIONS; +use crate::op_stack::OpStackElement; use crate::table::challenges::ChallengeId; use crate::table::challenges::ChallengeId::*; use crate::table::challenges::Challenges; @@ -178,20 +178,14 @@ impl ProcessorTable { .inverse(); } - // OpStack table - let clk = current_row[CLK.base_table_index()]; - let ib1 = current_row[IB1.base_table_index()]; - let osp = current_row[OpStackPointer.base_table_index()]; - let osv = current_row[ST15.base_table_index()]; // todo - let compressed_row_for_op_stack_table_permutation_argument = clk - * challenges[OpStackClkWeight] - + ib1 * challenges[OpStackIb1Weight] - + osp * challenges[OpStackPointerWeight] - + osv * challenges[OpStackFirstUnderflowElementWeight]; - op_stack_table_running_product *= challenges[OpStackIndeterminate] - - compressed_row_for_op_stack_table_permutation_argument; + op_stack_table_running_product *= Self::factor_for_op_stack_table_running_product( + previous_row, + current_row, + challenges, + ); // RAM Table + let clk = current_row[CLK.base_table_index()]; let ramv = current_row[RAMV.base_table_index()]; let ramp = current_row[RAMP.base_table_index()]; let previous_instruction = current_row[PreviousInstruction.base_table_index()]; @@ -357,6 +351,80 @@ impl ProcessorTable { previous_row = Some(current_row); } } + + fn factor_for_op_stack_table_running_product( + maybe_previous_row: Option>, + current_row: ArrayView1, + challenges: &Challenges, + ) -> XFieldElement { + let default_factor = XFieldElement::one(); + + let is_padding_row = current_row[IsPadding.base_table_index()].is_one(); + if is_padding_row { + return default_factor; + } + + let Some(previous_row) = maybe_previous_row else { + return default_factor; + }; + + let previous_opcode = previous_row[CI.base_table_index()]; + let Ok(previous_instruction): Result = previous_opcode.try_into() else { + return default_factor; + }; + + // shorter stack means relevant information is on top of stack, i.e., in stack registers + let row_with_shorter_stack = match previous_instruction.grows_op_stack() { + true => previous_row.view(), + false => current_row.view(), + }; + let op_stack_delta = previous_instruction + .op_stack_size_influence() + .unsigned_abs() as usize; + + let mut factor = default_factor; + for op_stack_pointer_offset in 0..op_stack_delta { + let max_stack_element_index = OpStackElement::COUNT - 1; + let stack_element_index = max_stack_element_index - op_stack_pointer_offset; + let stack_element_column = Self::op_stack_column_by_index(stack_element_index); + let underflow_element = row_with_shorter_stack[stack_element_column.base_table_index()]; + + let op_stack_pointer = row_with_shorter_stack[OpStackPointer.base_table_index()]; + let offset = BFieldElement::new(op_stack_pointer_offset as u64); + let offset_op_stack_pointer = op_stack_pointer + offset; + + let clk = previous_row[CLK.base_table_index()]; + let ib1_shrink_stack = previous_row[IB1.base_table_index()]; + let compressed_row = clk * challenges[OpStackClkWeight] + + ib1_shrink_stack * challenges[OpStackIb1Weight] + + offset_op_stack_pointer * challenges[OpStackPointerWeight] + + underflow_element * challenges[OpStackFirstUnderflowElementWeight]; + factor *= challenges[OpStackIndeterminate] - compressed_row; + } + factor + } + + fn op_stack_column_by_index(index: usize) -> ProcessorBaseTableColumn { + match index { + 0 => ST0, + 1 => ST1, + 2 => ST2, + 3 => ST3, + 4 => ST4, + 5 => ST5, + 6 => ST6, + 7 => ST7, + 8 => ST8, + 9 => ST9, + 10 => ST10, + 11 => ST11, + 12 => ST12, + 13 => ST13, + 14 => ST14, + 15 => ST15, + i => panic!("Op Stack column index must be in [0, 15], not {i}."), + } + } } #[derive(Debug, Clone)] @@ -436,18 +504,8 @@ impl ExtProcessorTable { let running_evaluation_for_standard_output_is_initialized_correctly = ext_row(OutputTableEvalArg) - x_constant(EvalArg::default_initial()); - // op-stack table - let op_stack_indeterminate = challenge(OpStackIndeterminate); - let op_stack_ib1_weight = challenge(OpStackIb1Weight); - let op_stack_pointer_weight = challenge(OpStackPointerWeight); - // note: `clk` and `first_underflow_element` are already constrained to be 0, and - // `op_stack_pointer` to be 16 - let compressed_row_for_op_stack_table = - op_stack_ib1_weight * base_row(IB1) + op_stack_pointer_weight * constant(16); let running_product_for_op_stack_table_is_initialized_correctly = - ext_row(OpStackTablePermArg) - - x_constant(PermArg::default_initial()) - * (op_stack_indeterminate - compressed_row_for_op_stack_table); + ext_row(OpStackTablePermArg) - x_constant(PermArg::default_initial()); // ram table let ram_indeterminate = challenge(RamIndeterminate); @@ -792,6 +850,12 @@ impl ExtProcessorTable { let next_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(NextBaseRow(col.master_base_table_index())) }; + let curr_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(CurrentExtRow(col.master_ext_table_index())) + }; + let next_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(NextExtRow(col.master_ext_table_index())) + }; vec![ next_base_row(ST11) - curr_base_row(ST11), @@ -799,8 +863,8 @@ impl ExtProcessorTable { next_base_row(ST13) - curr_base_row(ST13), next_base_row(ST14) - curr_base_row(ST14), next_base_row(ST15) - curr_base_row(ST15), - // The OpStack pointer does not change. next_base_row(OpStackPointer) - curr_base_row(OpStackPointer), + next_ext_row(OpStackTablePermArg) - curr_ext_row(OpStackTablePermArg), ] } @@ -892,17 +956,33 @@ impl ExtProcessorTable { circuit_builder: &ConstraintCircuitBuilder, ) -> Vec> { let constant = |c: u32| circuit_builder.b_constant(c.into()); + let challenge = |c: ChallengeId| circuit_builder.challenge(c); let curr_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(CurrentBaseRow(col.master_base_table_index())) }; let next_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(NextBaseRow(col.master_base_table_index())) }; + let curr_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(CurrentExtRow(col.master_ext_table_index())) + }; + let next_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(NextExtRow(col.master_ext_table_index())) + }; + + let compressed_row_for_op_stack_permutation_argument = challenge(OpStackClkWeight) + * curr_base_row(CLK) + + challenge(OpStackIb1Weight) * curr_base_row(IB1) + + challenge(OpStackPointerWeight) * curr_base_row(OpStackPointer) + + challenge(OpStackFirstUnderflowElementWeight) * curr_base_row(ST15); + let factor_for_op_stack_permutation_argument = + challenge(OpStackIndeterminate) - compressed_row_for_op_stack_permutation_argument; + let running_product_op_stack_table_has_accumulated_last_element_of_current_row = + next_ext_row(OpStackTablePermArg) + - curr_ext_row(OpStackTablePermArg) * factor_for_op_stack_permutation_argument; vec![ - // The stack element in st1 is moved into st2. next_base_row(ST2) - curr_base_row(ST1), - // And so on... next_base_row(ST3) - curr_base_row(ST2), next_base_row(ST4) - curr_base_row(ST3), next_base_row(ST5) - curr_base_row(ST4), @@ -916,8 +996,8 @@ impl ExtProcessorTable { next_base_row(ST13) - curr_base_row(ST12), next_base_row(ST14) - curr_base_row(ST13), next_base_row(ST15) - curr_base_row(ST14), - // The OpStack pointer is incremented by 1. next_base_row(OpStackPointer) - (curr_base_row(OpStackPointer) + constant(1)), + running_product_op_stack_table_has_accumulated_last_element_of_current_row, ] } @@ -944,19 +1024,33 @@ impl ExtProcessorTable { circuit_builder: &ConstraintCircuitBuilder, ) -> Vec> { let constant = |c: u32| circuit_builder.b_constant(c.into()); + let challenge = |c: ChallengeId| circuit_builder.challenge(c); let curr_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(CurrentBaseRow(col.master_base_table_index())) }; let next_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(NextBaseRow(col.master_base_table_index())) }; + let curr_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(CurrentExtRow(col.master_ext_table_index())) + }; + let next_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(NextExtRow(col.master_ext_table_index())) + }; + let compressed_row_for_op_stack_permutation_argument = challenge(OpStackClkWeight) + * curr_base_row(CLK) + + challenge(OpStackIb1Weight) * curr_base_row(IB1) + + challenge(OpStackPointerWeight) * next_base_row(OpStackPointer) + + challenge(OpStackFirstUnderflowElementWeight) * next_base_row(ST15); + let factor_for_op_stack_permutation_argument = + challenge(OpStackIndeterminate) - compressed_row_for_op_stack_permutation_argument; + let running_product_op_stack_table_has_accumulated_last_element_of_next_row = + next_ext_row(OpStackTablePermArg) + - curr_ext_row(OpStackTablePermArg) * factor_for_op_stack_permutation_argument; vec![ - // The stack element in st4 is moved into st3. next_base_row(ST3) - curr_base_row(ST4), - // The stack element in st5 is moved into st4. next_base_row(ST4) - curr_base_row(ST5), - // And so on... next_base_row(ST5) - curr_base_row(ST6), next_base_row(ST6) - curr_base_row(ST7), next_base_row(ST7) - curr_base_row(ST8), @@ -967,8 +1061,8 @@ impl ExtProcessorTable { next_base_row(ST12) - curr_base_row(ST13), next_base_row(ST13) - curr_base_row(ST14), next_base_row(ST14) - curr_base_row(ST15), - // The OpStack pointer, is decremented by 1. next_base_row(OpStackPointer) - (curr_base_row(OpStackPointer) - constant(1)), + running_product_op_stack_table_has_accumulated_last_element_of_next_row, // The helper variable register hv0 holds the inverse of (`op_stack_pointer` - 16). (curr_base_row(OpStackPointer) - constant(16)) * curr_base_row(HV0) - constant(1), ] @@ -2323,29 +2417,6 @@ impl ExtProcessorTable { + write_io_deselector * running_evaluation_updates } - fn running_product_for_op_stack_table_updates_correctly( - circuit_builder: &ConstraintCircuitBuilder, - ) -> ConstraintCircuitMonad { - let challenge = |c: ChallengeId| circuit_builder.challenge(c); - let next_base_row = |col: ProcessorBaseTableColumn| { - circuit_builder.input(NextBaseRow(col.master_base_table_index())) - }; - let curr_ext_row = |col: ProcessorExtTableColumn| { - circuit_builder.input(CurrentExtRow(col.master_ext_table_index())) - }; - let next_ext_row = |col: ProcessorExtTableColumn| { - circuit_builder.input(NextExtRow(col.master_ext_table_index())) - }; - - let compressed_row = challenge(OpStackClkWeight) * next_base_row(CLK) - + challenge(OpStackIb1Weight) * next_base_row(IB1) - + challenge(OpStackPointerWeight) * next_base_row(OpStackPointer) - + challenge(OpStackFirstUnderflowElementWeight) * next_base_row(ST15); // todo!!! - - next_ext_row(OpStackTablePermArg) - - curr_ext_row(OpStackTablePermArg) * (challenge(OpStackIndeterminate) - compressed_row) - } - fn running_product_for_ram_table_updates_correctly( circuit_builder: &ConstraintCircuitBuilder, ) -> ConstraintCircuitMonad { @@ -2740,7 +2811,6 @@ impl ExtProcessorTable { Self::running_evaluation_for_standard_input_updates_correctly(circuit_builder), Self::log_derivative_for_instruction_lookup_updates_correctly(circuit_builder), Self::running_evaluation_for_standard_output_updates_correctly(circuit_builder), - Self::running_product_for_op_stack_table_updates_correctly(circuit_builder), Self::running_product_for_ram_table_updates_correctly(circuit_builder), Self::running_product_for_jump_stack_table_updates_correctly(circuit_builder), Self::running_evaluation_hash_input_updates_correctly(circuit_builder), @@ -2974,6 +3044,7 @@ impl<'a> Display for ExtProcessorTraceRow<'a> { #[cfg(test)] pub(crate) mod tests { use ndarray::Array2; + use proptest::prelude::*; use rand::thread_rng; use rand::Rng; use strum::IntoEnumIterator; @@ -3772,4 +3843,21 @@ pub(crate) mod tests { let circuit_builder = ConstraintCircuitBuilder::new(); ExtProcessorTable::indicator_polynomial(&circuit_builder, index); } + + #[test] + fn can_get_op_stack_column_for_in_range_index() { + for index in 0..OpStackElement::COUNT { + let _ = ProcessorTable::op_stack_column_by_index(index); + } + } + + proptest! { + #[test] + #[should_panic(expected="[0, 15]")] + fn cannot_get_op_stack_column_for_out_of_range_index( + index in OpStackElement::COUNT.., + ) { + let _ = ProcessorTable::op_stack_column_by_index(index); + } + } } From 7418502b577d1d40bc82a056351fba45371d922a Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 13:09:28 +0200 Subject: [PATCH 14/27] test: op stack table row sorting --- triton-vm/src/table/op_stack_table.rs | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/triton-vm/src/table/op_stack_table.rs b/triton-vm/src/table/op_stack_table.rs index 56ba83446..6a1d48691 100644 --- a/triton-vm/src/table/op_stack_table.rs +++ b/triton-vm/src/table/op_stack_table.rs @@ -569,4 +569,48 @@ pub(crate) mod tests { prop_assert!(all_clk_values_are_clk); } } + + proptest! { + #[test] + fn compare_rows_with_unequal_stack_pointer_and_equal_clk( + stack_pointer_0: u64, + stack_pointer_1: u64, + clk: u64, + ) { + let mut row_0 = Array1::zeros(BASE_WIDTH); + row_0[StackPointer.base_table_index()] = stack_pointer_0.into(); + row_0[CLK.base_table_index()] = clk.into(); + + let mut row_1 = Array1::zeros(BASE_WIDTH); + row_1[StackPointer.base_table_index()] = stack_pointer_1.into(); + row_1[CLK.base_table_index()] = clk.into(); + + let stack_pointer_comparison = stack_pointer_0.cmp(&stack_pointer_1); + let row_comparison = OpStackTable::compare_rows(row_0.view(), row_1.view()); + + assert_eq!(stack_pointer_comparison, row_comparison); + } + } + + proptest! { + #[test] + fn compare_rows_with_equal_stack_pointer_and_unequal_clk( + stack_pointer: u64, + clk_0: u64, + clk_1: u64, + ) { + let mut row_0 = Array1::zeros(BASE_WIDTH); + row_0[StackPointer.base_table_index()] = stack_pointer.into(); + row_0[CLK.base_table_index()] = clk_0.into(); + + let mut row_1 = Array1::zeros(BASE_WIDTH); + row_1[StackPointer.base_table_index()] = stack_pointer.into(); + row_1[CLK.base_table_index()] = clk_1.into(); + + let clk_comparison = clk_0.cmp(&clk_1); + let row_comparison = OpStackTable::compare_rows(row_0.view(), row_1.view()); + + assert_eq!(clk_comparison, row_comparison); + } + } } From e84d6a0000a80d002c96dbf7bcf4d5905df45710 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 15:27:38 +0200 Subject: [PATCH 15/27] =?UTF-8?q?fix:=20processor's=20lookup=20multiplicit?= =?UTF-8?q?y=20of=20clock=20jump=20difference=20=E2=80=9C1=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Op Stack Table now has a padding indicator. It does not generate spurious clock jump difference lookup any longer. Hence, the Processor Table does not need to take them into account. --- triton-vm/src/table/processor_table.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/triton-vm/src/table/processor_table.rs b/triton-vm/src/table/processor_table.rs index a3c0871aa..b457e8b98 100644 --- a/triton-vm/src/table/processor_table.rs +++ b/triton-vm/src/table/processor_table.rs @@ -113,11 +113,12 @@ impl ProcessorTable { processor_table.slice_mut(s![processor_table_len.., CLK.base_table_index()]), ); - // The 3 memory-like tables do not have a padding indicator. Hence, clock jump differences - // are being looked up in their padding sections. The clock jump differences in that - // section are always 1. The lookup multiplicities of clock value 1 must be increased - // accordingly: one per padding row, times the number of memory-like tables, which is 3. - let num_padding_rows = 3 * (processor_table.nrows() - processor_table_len); + // The memory-like tables “RAM” and “Jump Stack” do not have a padding indicator. Hence, + // clock jump differences are being looked up in their padding sections. The clock jump + // differences in that section are always 1. The lookup multiplicities of clock value 1 must + // be increased accordingly: one per padding row, times the number of memory-like tables + // without padding indicator, which is 2. + let num_padding_rows = 2 * (processor_table.nrows() - processor_table_len); let num_pad_rows = BFieldElement::new(num_padding_rows as u64); let mut row_1 = processor_table.row_mut(1); row_1[ClockJumpDifferenceLookupMultiplicity.base_table_index()] += num_pad_rows; From 49fb63a49fd235b6c45c62de77671aee4079ce6d Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 16:21:28 +0200 Subject: [PATCH 16/27] fix: record correct op stack pointers in co-processor call sequence --- triton-vm/src/op_stack.rs | 22 ++++++++++++ triton-vm/src/table/op_stack_table.rs | 48 +++++++++++++++------------ 2 files changed, 48 insertions(+), 22 deletions(-) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index 62344e05d..efa91644a 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -182,6 +182,28 @@ impl UnderflowIO { } } + /// Whether the sequence of underflow IOs consists of either only reads or only writes. + pub fn is_uniform_sequence(sequence: &[Self]) -> bool { + sequence.iter().all(|io| io.is_same_type_as(sequence[0])) + } + + fn is_same_type_as(&self, other: Self) -> bool { + matches!( + (self, other), + (&Self::Read(_), Self::Read(_)) | (&Self::Write(_), Self::Write(_)) + ) + } + + /// Whether the sequence of underflow IOs consists of only writes. + pub fn is_writing_sequence(sequence: &[Self]) -> bool { + sequence.iter().all(|io| io.grows_stack()) + } + + /// Whether the sequence of underflow IOs consists of only reads. + pub fn is_reading_sequence(sequence: &[Self]) -> bool { + sequence.iter().all(|io| io.shrinks_stack()) + } + pub fn shrinks_stack(&self) -> bool { match self { Self::Read(_) => true, diff --git a/triton-vm/src/table/op_stack_table.rs b/triton-vm/src/table/op_stack_table.rs index 6a1d48691..9d3f1b5f3 100644 --- a/triton-vm/src/table/op_stack_table.rs +++ b/triton-vm/src/table/op_stack_table.rs @@ -69,10 +69,18 @@ impl OpStackTableEntry { pub fn from_underflow_io_sequence( clk: u32, - mut op_stack_pointer: BFieldElement, + op_stack_pointer_after_sequence_execution: BFieldElement, mut underflow_io_sequence: Vec, ) -> Vec { UnderflowIO::canonicalize_sequence(&mut underflow_io_sequence); + assert!(UnderflowIO::is_uniform_sequence(&underflow_io_sequence)); + + let sequence_length: BFieldElement = + u32::try_from(underflow_io_sequence.len()).unwrap().into(); + let mut op_stack_pointer = match UnderflowIO::is_writing_sequence(&underflow_io_sequence) { + true => op_stack_pointer_after_sequence_execution - sequence_length, + false => op_stack_pointer_after_sequence_execution + sequence_length, + }; let mut op_stack_table_entries = vec![]; for underflow_io in underflow_io_sequence { if underflow_io.shrinks_stack() { @@ -393,8 +401,6 @@ impl OpStackTable { #[cfg(test)] pub(crate) mod tests { - use std::cmp::max; - use itertools::Itertools; use num_traits::Zero; use proptest::collection::vec; @@ -509,16 +515,10 @@ pub(crate) mod tests { fn op_stack_pointer_in_sequence_of_op_stack_table_entries( clk: u32, stack_pointer in OpStackElement::COUNT..1024, - base_field_elements in vec(arb::(), 0..32), + base_field_elements in vec(arb::(), 0..OpStackElement::COUNT), sequence_of_writes: bool, ) { - let sequence_length = base_field_elements.len(); - let stack_pointer = match sequence_of_writes { - true => stack_pointer, - false => max(stack_pointer, sequence_length), - }; - - let sequence_length = u64::try_from(sequence_length).unwrap(); + let sequence_length = u64::try_from(base_field_elements.len()).unwrap(); let stack_pointer = u64::try_from(stack_pointer).unwrap(); let underflow_io_operation = match sequence_of_writes { @@ -539,17 +539,11 @@ pub(crate) mod tests { .sorted() .collect_vec(); - let expected_lowest_stack_pointer = match sequence_of_writes { - true => stack_pointer, - false => stack_pointer - sequence_length, + let expected_stack_pointer_range = match sequence_of_writes { + true => stack_pointer - sequence_length..stack_pointer, + false => stack_pointer..stack_pointer + sequence_length, }; - let expected_largest_stack_pointer = match sequence_of_writes { - true => stack_pointer + sequence_length, - false => stack_pointer, - }; - let expected_op_stack_pointers = - (expected_lowest_stack_pointer..expected_largest_stack_pointer).collect_vec(); - + let expected_op_stack_pointers = expected_stack_pointer_range.collect_vec(); prop_assert_eq!(expected_op_stack_pointers, op_stack_pointers); } } @@ -559,8 +553,18 @@ pub(crate) mod tests { fn clk_stays_same_in_sequence_of_op_stack_table_entries( clk: u32, stack_pointer in OpStackElement::COUNT..1024, - underflow_io in vec(arb::(), 0..OpStackElement::COUNT), + base_field_elements in vec(arb::(), 0..OpStackElement::COUNT), + sequence_of_writes: bool, ) { + let underflow_io_operation = match sequence_of_writes { + true => UnderflowIO::Write, + false => UnderflowIO::Read, + }; + let underflow_io = base_field_elements + .into_iter() + .map(underflow_io_operation) + .collect(); + let op_stack_pointer = u64::try_from(stack_pointer).unwrap().into(); let entries = OpStackTableEntry::from_underflow_io_sequence(clk, op_stack_pointer, underflow_io); From eea37ca7fefe7a58cabf2b789a024b7250f9dd2f Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 17:05:51 +0200 Subject: [PATCH 17/27] fix: delete spurious stack underflow write from `divine_sibling` --- triton-vm/src/vm.rs | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/triton-vm/src/vm.rs b/triton-vm/src/vm.rs index 977575d3a..6e0fc19c7 100644 --- a/triton-vm/src/vm.rs +++ b/triton-vm/src/vm.rs @@ -485,35 +485,26 @@ impl<'pgm> VMState<'pgm> { } fn divine_sibling(&mut self) -> Result> { - let (_st0_through_st4, underflow_io) = self.op_stack.pop_multiple::<{ DIGEST_LENGTH }>()?; - let mut all_underflow_io = underflow_io.to_vec(); - let (known_digest, underflow_io) = self.op_stack.pop_multiple()?; - all_underflow_io.extend(underflow_io); - - let (node_index, underflow_io) = self.op_stack.pop_u32()?; - all_underflow_io.push(underflow_io); + let _st0_through_st4 = self.op_stack.pop_multiple::<{ DIGEST_LENGTH }>()?; + let (known_digest, _) = self.op_stack.pop_multiple()?; + let (node_index, _) = self.op_stack.pop_u32()?; let parent_node_index = node_index / 2; - let underflow_io = self.op_stack.push(parent_node_index.into()); - all_underflow_io.push(underflow_io); + let _ = self.op_stack.push(parent_node_index.into()); let sibling_digest = self.pop_secret_digest()?; let (left_digest, right_digest) = Self::put_known_digest_on_correct_side(node_index, known_digest, sibling_digest); for &digest_element in right_digest.iter().rev() { - let underflow_io = self.op_stack.push(digest_element); - all_underflow_io.push(underflow_io); + let _ = self.op_stack.push(digest_element); } for &digest_element in left_digest.iter().rev() { - let underflow_io = self.op_stack.push(digest_element); - all_underflow_io.push(underflow_io); + let _ = self.op_stack.push(digest_element); } - let op_stack_calls = self.underflow_io_sequence_to_co_processor_calls(vec![underflow_io]); - self.instruction_pointer += 1; - Ok(op_stack_calls) + Ok(vec![]) } fn assert_vector(&mut self) -> Result> { From 4efffde3b21d4077ac1ccb3ac5bf731ebfec7ba8 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 22:15:23 +0200 Subject: [PATCH 18/27] test: stack underflow i/o sequence properties --- triton-vm/src/op_stack.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/triton-vm/src/op_stack.rs b/triton-vm/src/op_stack.rs index efa91644a..b8f66e9da 100644 --- a/triton-vm/src/op_stack.rs +++ b/triton-vm/src/op_stack.rs @@ -360,6 +360,7 @@ impl From<&OpStackElement> for BFieldElement { #[cfg(test)] mod tests { + use proptest::collection::vec; use proptest::prelude::*; use proptest_arbitrary_interop::arb; use twenty_first::shared_math::b_field_element::BFieldElement; @@ -487,4 +488,20 @@ mod tests { assert!(shrinks_stack ^ grows_stack); } } + + proptest! { + #[test] + fn non_empty_uniform_underflow_io_sequence_is_either_reading_or_writing( + sequence in vec(arb::(), 1..OpStackElement::COUNT), + ) { + let is_reading_sequence = UnderflowIO::is_reading_sequence(&sequence); + let is_writing_sequence = UnderflowIO::is_writing_sequence(&sequence); + if UnderflowIO::is_uniform_sequence(&sequence) { + prop_assert!(is_reading_sequence ^ is_writing_sequence); + } else { + prop_assert!(!is_reading_sequence); + prop_assert!(!is_writing_sequence); + } + } + } } From 224e79232525e3fb342f6a9dfb9f9cef3d29cefd Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Mon, 23 Oct 2023 22:44:45 +0200 Subject: [PATCH 19/27] test: factor for running product with Op Stack Table never panics --- triton-vm/src/table/challenges.rs | 2 ++ triton-vm/src/table/processor_table.rs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/triton-vm/src/table/challenges.rs b/triton-vm/src/table/challenges.rs index 6e7b5f81e..774a1ac29 100644 --- a/triton-vm/src/table/challenges.rs +++ b/triton-vm/src/table/challenges.rs @@ -18,6 +18,7 @@ //! table. Instead, the terminal of the Evaluation Argument is computed directly from the //! public input (respectively output) and the indeterminate. +use arbitrary::Arbitrary; use std::fmt::Debug; use std::hash::Hash; use std::ops::Index; @@ -216,6 +217,7 @@ impl ChallengeId { /// known only at runtime. The challenges are indexed using enum [`ChallengeId`]. The `Challenges` /// struct is essentially a thin wrapper around an array of [`XFieldElement`]s, providing /// convenience methods. +#[derive(Debug, Clone, Arbitrary)] pub struct Challenges { pub challenges: [XFieldElement; Self::count()], } diff --git a/triton-vm/src/table/processor_table.rs b/triton-vm/src/table/processor_table.rs index b457e8b98..27be66005 100644 --- a/triton-vm/src/table/processor_table.rs +++ b/triton-vm/src/table/processor_table.rs @@ -3045,7 +3045,9 @@ impl<'a> Display for ExtProcessorTraceRow<'a> { #[cfg(test)] pub(crate) mod tests { use ndarray::Array2; + use proptest::collection::vec; use proptest::prelude::*; + use proptest_arbitrary_interop::arb; use rand::thread_rng; use rand::Rng; use strum::IntoEnumIterator; @@ -3861,4 +3863,26 @@ pub(crate) mod tests { let _ = ProcessorTable::op_stack_column_by_index(index); } } + + proptest! { + #[test] + fn constructing_factor_for_op_stack_table_running_product_never_panics( + has_previous_row: bool, + previous_row in vec(arb::(), BASE_WIDTH), + current_row in vec(arb::(), BASE_WIDTH), + challenges in arb::(), + ) { + let previous_row = Array1::from(previous_row); + let current_row = Array1::from(current_row); + let maybe_previous_row = match has_previous_row { + true => Some(previous_row.view()), + false => None, + }; + let _ = ProcessorTable::factor_for_op_stack_table_running_product( + maybe_previous_row, + current_row.view(), + &challenges + ); + } + } } From eb8dc84054457eaf97b8d11c83588c56e2faa064 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 24 Oct 2023 09:42:02 +0200 Subject: [PATCH 20/27] =?UTF-8?q?doc:=20consistently=20use=20a=20space=20i?= =?UTF-8?q?n=20=E2=80=9Cop=20stack=E2=80=9D=20and=20=E2=80=9Cjump=20stack?= =?UTF-8?q?=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...lock-jump-differences-and-inner-sorting.md | 2 +- .../contiguity-of-memory-pointer-regions.md | 8 +- specification/src/data-structures.md | 8 +- specification/src/hash-table.md | 2 +- specification/src/instruction-groups.md | 14 ++-- ...ruction-specific-transition-constraints.md | 4 +- specification/src/instructions.md | 74 +++++++++---------- specification/src/jump-stack-table.md | 2 +- specification/src/memory-consistency.md | 4 +- specification/src/processor-table.md | 8 +- specification/src/registers.md | 8 +- 11 files changed, 67 insertions(+), 67 deletions(-) diff --git a/specification/src/clock-jump-differences-and-inner-sorting.md b/specification/src/clock-jump-differences-and-inner-sorting.md index ddfb4dec0..652ad87f9 100644 --- a/specification/src/clock-jump-differences-and-inner-sorting.md +++ b/specification/src/clock-jump-differences-and-inner-sorting.md @@ -1,6 +1,6 @@ # Clock Jump Differences and Inner Sorting -The previous sections show how it is proven that in the JumpStack, OpStack, and RAM Tables, the regions of constant memory pointer are contiguous. The next step is to prove that within each contiguous region of constant memory pointer, the rows are sorted for clock cycle. That is the topic of this section. +The previous sections show how it is proven that in the Jump Stack, Op Stack, and RAM Tables, the regions of constant memory pointer are contiguous. The next step is to prove that within each contiguous region of constant memory pointer, the rows are sorted for clock cycle. That is the topic of this section. The problem arises from *clock jumps*, which describes the phenomenon when the clock cycle increases even though the memory pointer does not change. If arbitrary jumps were allowed, nothing would prevent the cheating prover from using a table where higher rows correspond to later states, giving rise to an exploitable attack. diff --git a/specification/src/contiguity-of-memory-pointer-regions.md b/specification/src/contiguity-of-memory-pointer-regions.md index d4a08a5ee..877c5b47e 100644 --- a/specification/src/contiguity-of-memory-pointer-regions.md +++ b/specification/src/contiguity-of-memory-pointer-regions.md @@ -1,15 +1,15 @@ # Contiguity of Memory-Pointer Regions -## Contiguity for OpStack Table +## Contiguity for Op Stack Table -In each cycle, the memory pointer for the OpStack Table, `osp`, can only ever increase by one, remain the same, or decrease by one. As a result, it is easy to enforce that the entire table is sorted for memory pointer using one initial constraint and one transition constraint. +In each cycle, the memory pointer for the Op Stack Table, `osp`, can only ever increase by one, remain the same, or decrease by one. As a result, it is easy to enforce that the entire table is sorted for memory pointer using one initial constraint and one transition constraint. - Initial constraint: `osp` starts with zero, so in terms of polynomials the constraint is `osp`. - Transition constraint: the new `osp` is either the same as the previous or one larger. The polynomial representation for this constraint is `(osp' - osp - 1) * (osp' - osp)`. -## Contiguity for JumpStack Table +## Contiguity for Jump Stack Table -Analogously to the OpStack Table, the JumpStack's memory pointer `jsp` can only ever decrease by one, remain the same, or increase by one, within each cycle. As a result, similar constraints establish that the entire table is sorted for memory pointer. +Analogously to the Op Stack Table, the Jump Stack's memory pointer `jsp` can only ever decrease by one, remain the same, or increase by one, within each cycle. As a result, similar constraints establish that the entire table is sorted for memory pointer. - Initial constraint: `jsp` starts with zero, so in terms of polynomials the constraint is `jsp`. - Transition constraint: the new `jsp` is either the same as the previous or one larger. The polynomial representation for this constraint is `(jsp' - jsp - 1) * (jsp' - jsp)`. diff --git a/specification/src/data-structures.md b/specification/src/data-structures.md index 82a5f6c01..4fd55af6e 100644 --- a/specification/src/data-structures.md +++ b/specification/src/data-structures.md @@ -6,13 +6,13 @@ Regardless of the data structure, the address lives in the B-field. There are four separate notions of memory: 1. *RAM*, to which the VM can read and write field elements. 2. *Program Memory*, from which the VM reads instructions. -3. *OpStack Memory*, which stores the operational stack. +3. *Op Stack Memory*, which stores the operational stack. 4. *Jump Stack Memory*, which stores the entire jump stack. ## Operational Stack The stack is a last-in;first-out data structure that allows the program to store intermediate variables, pass arguments, and keep pointers to objects held in RAM. -In this document, the operational stack is either referred to as just “stack” or, if more clarity is desired, “OpStack.” +In this document, the operational stack is either referred to as just “stack” or, if more clarity is desired, “op stack.” From the Virtual Machine's point of view, the stack is a single, continuous object. The first 16 elements of the stack can be accessed very conveniently. @@ -20,9 +20,9 @@ Elements deeper in the stack require removing some of the higher elements, possi For reasons of arithmetization, the stack is actually split into two distinct parts: 1. the _operational stack registers_ `st0` through `st15`, and -1. the _OpStack Underflow Memory_. +1. the _Op Stack Underflow Memory_. -The motivation and the interplay between the two parts is described and exemplified in [arithmetization of the OpStack table](operational-stack-table.md). +The motivation and the interplay between the two parts is described and exemplified in [arithmetization of the Op Stack table](operational-stack-table.md). ## Random Access Memory diff --git a/specification/src/hash-table.md b/specification/src/hash-table.md index 61c6ebb21..de0f5f6ae 100644 --- a/specification/src/hash-table.md +++ b/specification/src/hash-table.md @@ -1,6 +1,6 @@ # Hash Table -The instruction `hash` hashes the OpStack's 10 top-most elements in one cycle. +The instruction `hash` hashes the Op Stack's 10 top-most elements in one cycle. Similarly, the Sponge instructions `sponge_init`, `sponge_absorb`, and `sponge_squeeze` also all complete in one cycle. The main processor achieves this by using a hash 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). diff --git a/specification/src/instruction-groups.md b/specification/src/instruction-groups.md index 46e2d1121..5f7ec4a86 100644 --- a/specification/src/instruction-groups.md +++ b/specification/src/instruction-groups.md @@ -197,8 +197,8 @@ Contains all constraints from instruction group `keep_jump_stack`, and additiona 1. The stack element in `st12` is moved into `st13`. 1. The stack element in `st13` is moved into `st14`. 1. The stack element in `st14` is moved into `st15`. -1. The stack element in `st15` is moved to the top of OpStack underflow, i.e., `osv`. -1. The OpStack pointer is incremented by 1. +1. The stack element in `st15` is moved to the top of op stack underflow, i.e., `osv`. +1. The op stack pointer is incremented by 1. ### Polynomials @@ -240,8 +240,8 @@ Contains all constraints from instruction group `stack_grows_and_top_2_unconstra 1. The stack element in `st13` does not change. 1. The stack element in `st14` does not change. 1. The stack element in `st15` does not change. -1. The top of the OpStack underflow, i.e., `osv`, does not change. -1. The OpStack pointer does not change. +1. The top of the op stack underflow, i.e., `osv`, does not change. +1. The op stack pointer does not change. ### Polynomials @@ -318,7 +318,7 @@ Contains all constraints from instruction group `unary_operation`, and additiona ## Group `stack_shrinks_and_top_3_unconstrained` This instruction group requires helper variable `hv0` to hold the multiplicative inverse of `(osp - 16)`. -In effect, this means that the OpStack pointer can only be decremented if it is not 16, i.e., if OpStack Underflow Memory is not empty. +In effect, this means that the op stack pointer can only be decremented if it is not 16, i.e., if op stack underflow memory is not empty. Since the stack can only change by one element at a time, this prevents stack underflow. ### Description @@ -335,8 +335,8 @@ Since the stack can only change by one element at a time, this prevents stack un 1. The stack element in `st13` is moved into `st12`. 1. The stack element in `st14` is moved into `st13`. 1. The stack element in `st15` is moved into `st14`. -1. The stack element at the top of OpStack underflow, i.e., `osv`, is moved into `st15`. -1. The OpStack pointer is decremented by 1. +1. The stack element at the top of op stack underflow, i.e., `osv`, is moved into `st15`. +1. The op stack pointer is decremented by 1. 1. The helper variable register `hv0` holds the inverse of `(osp - 16)`. ### Polynomials diff --git a/specification/src/instruction-specific-transition-constraints.md b/specification/src/instruction-specific-transition-constraints.md index 8c04cc081..a222a4684 100644 --- a/specification/src/instruction-specific-transition-constraints.md +++ b/specification/src/instruction-specific-transition-constraints.md @@ -134,8 +134,8 @@ Additionally, it defines the following transition constraints. 1. If `i` is not 13, then `st13` does not change. 1. If `i` is not 14, then `st14` does not change. 1. If `i` is not 15, then `st15` does not change. -1. The top of the OpStack underflow, i.e., `osv`, does not change. -1. The OpStack pointer does not change. +1. The top of the op stack underflow, i.e., `osv`, does not change. +1. The op stack pointer does not change. ### Polynomials diff --git a/specification/src/instructions.md b/specification/src/instructions.md index 185661981..d6427dee1 100644 --- a/specification/src/instructions.md +++ b/specification/src/instructions.md @@ -19,9 +19,9 @@ The second property helps guarantee that operational stack underflow cannot happ It is used by several instructions through instruction group [`stack_shrinks_and_top_3_unconstrained`](instruction-groups.md#group-stack_shrinks_and_top_3_unconstrained). The third property allows efficient arithmetization of the running product for the Permutation Argument between [Processor Table](processor-table.md) and [U32 Table](u32-table.md). -## OpStack Manipulation +## Op Stack Manipulation -| Instruction | Opcode | old OpStack | new OpStack | Description | +| Instruction | Opcode | old op stack | new op stack | Description | |:-------------|-------:|:--------------------|:----------------------|:---------------------------------------------------------------------------------| | `pop` | 2 | `_ a` | `_` | Pops top element from stack. | | `push` + `a` | 1 | `_` | `_ a` | Pushes `a` onto the stack. | @@ -42,26 +42,26 @@ the value `a` was supplied as a secret input. ## Control Flow -| Instruction | Opcode | old OpStack | new OpStack | old `ip` | new `ip` | old JumpStack | new JumpStack | Description | -|:-------------|-------:|:------------|:------------|:---------|:---------|:--------------|:--------------|:-------------------------------------------------------------------------------------------------------------------------| -| `nop` | 16 | `_` | `_` | `_` | `_ + 1` | `_` | `_` | Do nothing | -| `skiz` | 10 | `_ a` | `_` | `_` | `_ + s` | `_` | `_` | Skip next instruction if `a` is zero. `s` ∈ {1, 2, 3} depends on `a` and whether the next instruction takes an argument. | -| `call` + `d` | 25 | `_` | `_` | `o` | `d` | `_` | `_ (o+2, d)` | Push `(o+2,d)` to the jump stack, and jump to absolute address `d` | -| `return` | 24 | `_` | `_` | `_` | `o` | `_ (o, d)` | `_` | Pop one pair off the jump stack and jump to that pair's return address (which is the first element). | -| `recurse` | 32 | `_` | `_` | `_` | `d` | `_ (o, d)` | `_ (o, d)` | Peek at the top pair of the jump stack and jump to that pair's destination address (which is the second element). | -| `assert` | 18 | `_ a` | `_` | `_` | `_ + 1` | `_` | `_` | Pops `a` if `a == 1`, else crashes the virtual machine. | -| `halt` | 0 | `_` | `_` | `_` | `_ + 1` | `_` | `_` | Solves the halting problem (if the instruction is reached). Indicates graceful shutdown of the VM. | +| Instruction | Opcode | old op stack | new op stack | old `ip` | new `ip` | old jump stack | new jump stack | Description | +|:-------------|-------:|:-------------|:-------------|:---------|:---------|:---------------|:---------------|:-------------------------------------------------------------------------------------------------------------------------| +| `nop` | 16 | `_` | `_` | `_` | `_ + 1` | `_` | `_` | Do nothing | +| `skiz` | 10 | `_ a` | `_` | `_` | `_ + s` | `_` | `_` | Skip next instruction if `a` is zero. `s` ∈ {1, 2, 3} depends on `a` and whether the next instruction takes an argument. | +| `call` + `d` | 25 | `_` | `_` | `o` | `d` | `_` | `_ (o+2, d)` | Push `(o+2,d)` to the jump stack, and jump to absolute address `d` | +| `return` | 24 | `_` | `_` | `_` | `o` | `_ (o, d)` | `_` | Pop one pair off the jump stack and jump to that pair's return address (which is the first element). | +| `recurse` | 32 | `_` | `_` | `_` | `d` | `_ (o, d)` | `_ (o, d)` | Peek at the top pair of the jump stack and jump to that pair's destination address (which is the second element). | +| `assert` | 18 | `_ a` | `_` | `_` | `_ + 1` | `_` | `_` | Pops `a` if `a == 1`, else crashes the virtual machine. | +| `halt` | 0 | `_` | `_` | `_` | `_ + 1` | `_` | `_` | Solves the halting problem (if the instruction is reached). Indicates graceful shutdown of the VM. | ## Memory Access -| Instruction | Opcode | old OpStack | new OpStack | old `ramv` | new `ramv` | Description | -|:------------|-------:|:------------|:------------|:-----------|:-----------|:----------------------------------------------------------------------------| -| `read_mem` | 40 | `_ p` | `_ p v` | `v` | `v` | Reads value `v` from RAM at address `p` and pushes `v` onto the OpStack. | -| `write_mem` | 26 | `_ p v` | `_ p` | `_` | `v` | Writes OpStack's top-most value `v` to RAM at the address `p`, popping `v`. | +| Instruction | Opcode | old op stack | new op stack | old `ramv` | new `ramv` | Description | +|:------------|-------:|:-------------|:-------------|:-----------|:-----------|:-----------------------------------------------------------------------------| +| `read_mem` | 40 | `_ p` | `_ p v` | `v` | `v` | Reads value `v` from RAM at address `p` and pushes `v` onto the op stack. | +| `write_mem` | 26 | `_ p v` | `_ p` | `_` | `v` | Writes op stack's top-most value `v` to RAM at the address `p`, popping `v`. | ## Hashing -| Instruction | Opcode | old OpStack | new OpStack | Description | +| Instruction | Opcode | old op stack | new op stack | Description | |:-----------------|-------:|:----------------|:------------------------------|:--------------------------------------------------------------------------------------------------------| | `hash` | 48 | `_jihgfedcba` | `_yxwvu00000` | Overwrites the stack's 10 top-most elements with their hash digest (length 5) and 5 zeros. | | `divine_sibling` | 56 | `_ iedcba*****` | e.g., `_ (i div 2)edcbazyxwv` | Helps traversing a Merkle tree during authentication path verification. See extended description below. | @@ -98,29 +98,29 @@ Triton VM cannot know the number of elements that will be absorbed. ## Base Field Arithmetic on Stack -| Instruction | Opcode | old OpStack | new OpStack | Description | -|:------------|-------:|:------------|:-------------|:---------------------------------------------------------------------------------------------------------------------------| -| `add` | 34 | `_ b a` | `_ c` | Computes the sum (`c`) of the top two elements of the stack (`b` and `a`) over the field. | -| `mul` | 42 | `_ b a` | `_ c` | Computes the product (`c`) of the top two elements of the stack (`b` and `a`) over the field. | -| `invert` | 96 | `_ a` | `_ b` | Computes the multiplicative inverse (over the field) of the top of the stack. Crashes the VM if the top of the stack is 0. | -| `eq` | 50 | `_ b a` | `_ (a == b)` | Tests the top two stack elements for equality. | +| Instruction | Opcode | old op stack | new op stack | Description | +|:------------|-------:|:-------------|:-------------|:---------------------------------------------------------------------------------------------------------------------------| +| `add` | 34 | `_ b a` | `_ c` | Computes the sum (`c`) of the top two elements of the stack (`b` and `a`) over the field. | +| `mul` | 42 | `_ b a` | `_ c` | Computes the product (`c`) of the top two elements of the stack (`b` and `a`) over the field. | +| `invert` | 96 | `_ a` | `_ b` | Computes the multiplicative inverse (over the field) of the top of the stack. Crashes the VM if the top of the stack is 0. | +| `eq` | 50 | `_ b a` | `_ (a == b)` | Tests the top two stack elements for equality. | ## Bitwise Arithmetic on Stack -| Instruction | Opcode | old OpStack | new OpStack | Description | -|:--------------|-------:|:------------|:--------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `split` | 4 | `_ a` | `_ hi lo` | Decomposes the top of the stack into the lower 32 bits and the upper 32 bits. | -| `lt` | 6 | `_ b a` | `_ a Date: Tue, 24 Oct 2023 15:43:29 +0200 Subject: [PATCH 21/27] doc: prose and example for Op Stack Table behavior --- specification/src/data-structures.md | 15 +- specification/src/operational-stack-table.md | 155 ++++++++-------- specification/src/processor-table.md | 13 ++ triton-vm/src/instruction.rs | 2 +- triton-vm/src/stark.rs | 175 ++++++++++++++----- 5 files changed, 241 insertions(+), 119 deletions(-) diff --git a/specification/src/data-structures.md b/specification/src/data-structures.md index 4fd55af6e..7f78774af 100644 --- a/specification/src/data-structures.md +++ b/specification/src/data-structures.md @@ -1,13 +1,20 @@ # Data Structures ## Memory + The term *memory* refers to a data structure that gives read access (and possibly write access, too) to elements indicated by an *address*. Regardless of the data structure, the address lives in the B-field. There are four separate notions of memory: + +1. *Program Memory*, from which the VM reads instructions. +1. *Op Stack Memory*, which stores the operational stack. 1. *RAM*, to which the VM can read and write field elements. -2. *Program Memory*, from which the VM reads instructions. -3. *Op Stack Memory*, which stores the operational stack. -4. *Jump Stack Memory*, which stores the entire jump stack. +1. *Jump Stack Memory*, which stores the entire jump stack. + +## Program Memory + +Program memory holds the instructions of the program currently executed by Triton VM. +It is immutable. ## Operational Stack @@ -36,8 +43,10 @@ This initialization is one form of secret input, and one of two mechanisms that The other mechanism is [dedicated instructions](instructions.md#opstack-manipulation). ## Jump Stack + Another last-in;first-out data structure that keeps track of return and destination addresses. This stack changes only when control follows a `call` or `return` instruction. +Furthermore, executing instruction `recurse` requires a non-empty jump stack. --- diff --git a/specification/src/operational-stack-table.md b/specification/src/operational-stack-table.md index 48445b31f..a392cb0f9 100644 --- a/specification/src/operational-stack-table.md +++ b/specification/src/operational-stack-table.md @@ -1,97 +1,104 @@ # Operational Stack Table -The operational stack is where the program stores simple elementary operations, function arguments, and pointers to important objects. +The [operational stack](data-structures.md#operational-stack)[^abbrev] is where the program stores simple elementary operations, function arguments, and pointers to important objects. There are 16 registers (`st0` through `st15`) that the program can access directly. These registers correspond to the top of the stack. They are recorded in the [Processor Table](processor-table.md). -The rest of the operational stack is stored in a dedicated memory object called “operational stack underflow memory”. +The rest of the op stack is stored in a dedicated memory object called “operational stack underflow memory”. It is initially empty. -The evolution of the underflow memory is recorded in the Operational Stack Table. +The evolution of the underflow memory is recorded in the Op Stack Table. +The sole task of the Op Stack Table is to keep underflow memory immutable. +To achieve this, any read or write accesses to the underflow memory are recorded in the Op Stack Table. +Read and write accesses to op stack underflow memory are a side effect of shrinking or growing the op stack. ## Base Columns -The Operational Stack Table consists of 4 columns: +The Op Stack Table consists of 4 columns: 1. the cycle counter `clk` -1. the shrink stack indicator `shrink_stack` -1. the operational stack value `osv`, and -1. the operational stack pointer `osp`. +1. the shrink stack indicator `shrink_stack`, corresponding to the processor's instruction bit 1 `ib1`, +1. the op stack pointer `stack_pointer`, and +1. the first underflow element `first_underflow_element`. -| Clock | Shrink Stack Indicator | Op Stack Pointer | Op Stack Value | -|:------|:-----------------------|:-----------------|:---------------| -| - | - | - | - | +| Clock | Shrink Stack Indicator | Stack Pointer | First Underflow Element | +|:------|:-----------------------|:--------------|:------------------------| +| - | - | - | - | -Columns `clk`, `shrink_stack`, `osp`, and `osv` correspond to columns `clk`, `ib1`, `osp`, and `osv` in the [Processor Table](processor-table.md), respectively. -A [Permutation Argument](permutation-argument.md) with the Processor Table establishes that, selecting the columns with these labels, the two tables' sets of rows are identical. +Column `clk` records the processor's execution cycle during which the read or write access happens. +The `shrink_stack` indicator signals whether the underflow memory access is a read or a write: +a read corresponds to a shrinking stack is indicated by a 1, a write corresponds to a growing stack and is indicated by a 0. +The same column doubles up as a [padding indicator](#padding), in which case `shrink_stack` is set to 2. +The `stack_pointer` is the address at which the to-be-read-or-written element resides. +Finally, the `first_underflow_element` is the stack element being transferred from stack register `st15` into underflow memory on a write, or the other way around on a read. -In order to guarantee [memory consistency](memory-consistency.md), the rows of the operational stack table are sorted by operational stack pointer `osp` first, cycle count `clk` second. +A [subset Permutation Argument](permutation-argument.md) with the [Processor Table](processor-table.md) establishes that the rows recorded in the Op Stack Table are consistent with the processor's part of the op stack. + +In order to guarantee [memory consistency](memory-consistency.md), the rows of the operational stack table are sorted by `stack_pointer` first, cycle count `clk` second. The mechanics are best illustrated by an example. Observe how the malicious manipulation of the Op Stack Underflow Memory, the change of “42” into “99” in cycle 8, is detected: -The transition constraints of the Op Stack Table stipulate that if `osp` does not change, then `osv` can only change if the shrink stack indicator `shrink_stack` is set. -Consequently, row `[5, push, 9, 42]` being followed by row `[_, _, 9, 99]` violates the constraints. +The transition constraints of the Op Stack Table stipulate that if `stack_pointer` does not change, then the `first_underflow_element` can only change if the next instruction grows the stack. +Consequently, row `[4, 0, 8, 42]` being followed by row `[10, 1, 8, 99]` violates the constraints. The shrink stack indicator being correct is guaranteed by the Permutation Argument between Op Stack Table and the [Processor Table](processor-table.md). For illustrative purposes only, we use four stack registers `st0` through `st3` in the example. -TritonVM has 16 stack registers, `st0` through `st15`. - -Execution trace: - -| `clk` | `ci` | `nia` | `st0` | `st1` | `st2` | `st3` | Op Stack Underflow Memory | `osp` | `osv` | -|------:|:-------|:-------|------:|------:|------:|------:|:--------------------------|------:|------:| -| 0 | `push` | 42 | 0 | 0 | 0 | 0 | [ ] | 4 | 0 | -| 1 | `push` | 43 | 42 | 0 | 0 | 0 | [0] | 5 | 0 | -| 2 | `push` | 44 | 43 | 42 | 0 | 0 | [0,0] | 6 | 0 | -| 3 | `push` | 45 | 44 | 43 | 42 | 0 | [0,0,0] | 7 | 0 | -| 4 | `push` | 46 | 45 | 44 | 43 | 42 | [0,0,0,0] | 8 | 0 | -| 5 | `push` | 47 | 46 | 45 | 44 | 43 | [42,0,0,0,0] | 9 | 42 | -| 6 | `push` | 48 | 47 | 46 | 45 | 44 | [43,42,0,0,0,0] | 10 | 43 | -| 7 | `nop` | `pop` | 48 | 47 | 46 | 45 | [44,43,42,0,0,0,0] | 11 | 44 | -| 8 | `pop` | `pop` | 48 | 47 | 46 | 45 | [44,43,99,0,0,0,0] | 11 | 44 | -| 9 | `pop` | `pop` | 47 | 46 | 45 | 44 | [43,99,0,0,0,0] | 10 | 43 | -| 10 | `pop` | `pop` | 46 | 45 | 44 | 43 | [99,0,0,0,0] | 9 | 99 | -| 11 | `pop` | `push` | 45 | 44 | 43 | 99 | [0,0,0,0] | 8 | 0 | -| 12 | `push` | 77 | 44 | 43 | 99 | 0 | [0,0,0] | 7 | 0 | -| 13 | `swap` | 4 | 77 | 44 | 43 | 99 | [0,0,0,0] | 8 | 0 | -| 14 | `push` | 78 | 99 | 44 | 43 | 77 | [0,0,0,0] | 8 | 0 | -| 15 | `swap` | 4 | 78 | 99 | 44 | 43 | [77,0,0,0,0] | 9 | 77 | -| 16 | `push` | 79 | 43 | 99 | 44 | 78 | [77,0,0,0,0] | 9 | 77 | -| 17 | `pop` | `pop` | 79 | 43 | 99 | 44 | [78,77,0,0,0,0] | 10 | 78 | -| 18 | `pop` | `pop` | 43 | 99 | 44 | 78 | [77,0,0,0,0] | 9 | 77 | -| 19 | `pop` | `pop` | 99 | 44 | 78 | 77 | [0,0,0,0] | 8 | 0 | -| 20 | `pop` | `pop` | 44 | 78 | 77 | 0 | [0,0,0] | 7 | 0 | -| 21 | `pop` | `pop` | 78 | 77 | 0 | 0 | [0,0] | 6 | 0 | -| 22 | `pop` | `pop` | 77 | 0 | 0 | 0 | [0] | 5 | 0 | -| 23 | `pop` | `pop` | 0 | 0 | 0 | 0 | [ ] | 4 | 0 | -| 24 | `pop` | `nop` | 0 | 0 | 0 | 0 | 💥 | 3 | 0 | +Triton VM has 16 stack registers, `st0` through `st15`. +Furthermore, implied next instructions usually recorded in register “next instruction or argument” `nia` are omitted for reasons of readability. + +Processor's execution trace: + +| `clk` | `ci` | `nia` | `st0` | `st1` | `st2` | `st3` | Op Stack Underflow Memory | `op_stack_pointer` | +|------:|:-----|------:|------:|------:|------:|------:|:--------------------------|-------------------:| +| 0 | push | 42 | 0 | 0 | 0 | 0 | [] | 4 | +| 1 | push | 43 | 42 | 0 | 0 | 0 | [ 0] | 5 | +| 2 | push | 44 | 43 | 42 | 0 | 0 | [ 0, 0] | 6 | +| 3 | push | 45 | 44 | 43 | 42 | 0 | [ 0, 0, 0] | 7 | +| 4 | push | 46 | 45 | 44 | 43 | 42 | [ 0, 0, 0, 0] | 8 | +| 5 | push | 47 | 46 | 45 | 44 | 43 | [42, 0, 0, 0, 0] | 9 | +| 6 | push | 48 | 47 | 46 | 45 | 44 | [43, 42, 0, 0, 0, 0] | 10 | +| 7 | nop | | 48 | 47 | 46 | 45 | [44, 43, 42, 0, 0, 0] | 11 | +| 8 | pop | | 48 | 47 | 46 | 45 | [44, 43, 99, 0, 0, 0] | 11 | +| 9 | pop | | 47 | 46 | 45 | 44 | [43, 99, 0, 0, 0, 0] | 10 | +| 10 | pop | | 46 | 45 | 44 | 43 | [99, 0, 0, 0, 0] | 9 | +| 11 | pop | | 45 | 44 | 43 | 99 | [ 0, 0, 0, 0] | 8 | +| 12 | push | 77 | 44 | 43 | 99 | 0 | [ 0, 0, 0] | 7 | +| 13 | swap | 3 | 77 | 44 | 43 | 99 | [ 0, 0, 0, 0] | 8 | +| 14 | push | 78 | 99 | 44 | 43 | 77 | [ 0, 0, 0, 0] | 8 | +| 15 | swap | 3 | 78 | 99 | 44 | 43 | [77, 0, 0, 0, 0] | 9 | +| 16 | push | 79 | 43 | 99 | 44 | 78 | [77, 0, 0, 0, 0] | 9 | +| 17 | pop | | 79 | 43 | 99 | 44 | [78, 77, 0, 0, 0, 0] | 10 | +| 18 | pop | | 43 | 99 | 44 | 78 | [77, 0, 0, 0, 0] | 9 | +| 19 | pop | | 99 | 44 | 78 | 77 | [ 0, 0, 0, 0] | 8 | +| 20 | pop | | 44 | 78 | 77 | 0 | [ 0, 0, 0] | 7 | +| 21 | pop | | 78 | 77 | 0 | 0 | [ 0, 0] | 6 | +| 22 | pop | | 77 | 0 | 0 | 0 | [ 0] | 5 | +| 23 | halt | | 0 | 0 | 0 | 0 | [] | 4 | + Operational Stack Table: -| `clk` | `shrink_stack` | (comment) | `osp` | `osv` | -|------:|---------------:|:----------|------:|------:| -| 0 | 0 | (`push`) | 4 | 0 | -| 23 | 1 | (`pop`) | 4 | 0 | -| 1 | 0 | (`push`) | 5 | 0 | -| 22 | 1 | (`pop`) | 5 | 0 | -| 2 | 0 | (`push`) | 6 | 0 | -| 21 | 1 | (`pop`) | 6 | 0 | -| 3 | 0 | (`push`) | 7 | 0 | -| 12 | 0 | (`push`) | 7 | 0 | -| 20 | 1 | (`pop`) | 7 | 0 | -| 4 | 0 | (`push`) | 8 | 0 | -| 11 | 1 | (`pop`) | 8 | 0 | -| 13 | 0 | (`swap`) | 8 | 0 | -| 14 | 0 | (`push`) | 8 | 0 | -| 19 | 1 | (`pop`) | 8 | 0 | -| 5 | 0 | (`push`) | 9 | 42 | -| 10 | 1 | (`pop`) | 9 | 99 | -| 15 | 0 | (`swap`) | 9 | 77 | -| 16 | 0 | (`push`) | 9 | 77 | -| 18 | 1 | (`pop`) | 9 | 77 | -| 6 | 0 | (`push`) | 10 | 43 | -| 9 | 1 | (`pop`) | 10 | 43 | -| 17 | 1 | (`pop`) | 10 | 78 | -| 7 | 0 | (`nop`) | 11 | 44 | -| 8 | 1 | (`pop`) | 11 | 44 | +| `clk` | `shrink_stack` | `stack_pointer` | `first_underflow_element` | +|------:|---------------:|----------------:|--------------------------:| +| 0 | 0 | 4 | 0 | +| 22 | 1 | 4 | 0 | +| 1 | 0 | 5 | 0 | +| 21 | 1 | 5 | 0 | +| 2 | 0 | 6 | 0 | +| 20 | 1 | 6 | 0 | +| 3 | 0 | 7 | 0 | +| 11 | 1 | 7 | 0 | +| 12 | 0 | 7 | 0 | +| 19 | 1 | 7 | 0 | +| 4 | 0 | 8 | 42 | +| 10 | 1 | 8 | 99 | +| 14 | 0 | 8 | 77 | +| 18 | 1 | 8 | 77 | +| 5 | 0 | 9 | 43 | +| 9 | 1 | 9 | 43 | +| 16 | 0 | 9 | 78 | +| 17 | 1 | 9 | 78 | +| 6 | 0 | 10 | 44 | +| 8 | 1 | 10 | 44 | + ## Extension Columns @@ -177,3 +184,7 @@ Written as Disjunctive Normal Form, the same constraints can be expressed as: ## Terminal Constraints None. + +--- + +[^abbrev]: frequently abbreviated as “Op Stack” diff --git a/specification/src/processor-table.md b/specification/src/processor-table.md index fbb938d46..1de3b4ac7 100644 --- a/specification/src/processor-table.md +++ b/specification/src/processor-table.md @@ -34,6 +34,19 @@ The Processor Table has the following extension columns, corresponding to [Evalu 1. `U32LookupClientLogDerivative` for the Lookup Argument with the [U32 Table](u32-table.md). 1. `ClockJumpDifferenceLookupServerLogDerivative` for the Lookup Argument of clock jump differences with the [Op Stack Table](operational-stack-table.md), the [RAM Table](random-access-memory-table.md), and the [Jump Stack Table](jump-stack-table.md). +### Permutation Argument with the Op Stack Table + +The [Permutation Arguments](permutation-argument.md) with the [Op Stack Table](operational-stack-table.md) `RunningProductOpStackTable` establishes consistency of the op stack underflow memory. +The number of factors incorporated into the running product at any given cycle depends on the executed instruction in this cycle: +for every element pushed to or popped from the stack, there is one factor. +Namely, if the op stack grows, every element spilling from `st15` into op stack underflow memory will be incorporated as one factor; +and if the op stack shrinks, every element from op stack underflow memory being transferred into `st15` will be one factor. + +One key insight for this Permutation Argument is that the processor will always have access to the elements that are to be read from or written to underflow memory: +if the instruction grows the op stack, then the elements in question currently reside in the directly accessible, top part of the stack; +if the instruction shrinks the op stack, then the elements in question will be in the top part of the stack in the next cycle. +In either case, the [Transition Constraint](arithmetization.md#arithmetic-intermediate-representation) for the Permutation Argument can incorporate the explicitly listed elements as well as the corresponding trivial-to-compute `op_stack_pointer`. + ## Padding A padding row is a copy of the Processor Table's last row with the following modifications: diff --git a/triton-vm/src/instruction.rs b/triton-vm/src/instruction.rs index dac20ef6f..30f1af48f 100644 --- a/triton-vm/src/instruction.rs +++ b/triton-vm/src/instruction.rs @@ -197,7 +197,7 @@ impl AnInstruction { } } - const fn name(&self) -> &'static str { + pub(crate) const fn name(&self) -> &'static str { match self { Pop => "pop", Push(_) => "push", diff --git a/triton-vm/src/stark.rs b/triton-vm/src/stark.rs index acdecec5c..b7d1f8b8f 100644 --- a/triton-vm/src/stark.rs +++ b/triton-vm/src/stark.rs @@ -1115,10 +1115,13 @@ pub(crate) mod tests { use rand::thread_rng; use rand::Rng; use rand_core::RngCore; + use strum::EnumCount; use twenty_first::shared_math::other::random_elements; use crate::example_programs::*; use crate::instruction::AnInstruction; + use crate::instruction::Instruction; + use crate::op_stack::OpStackElement; use crate::program::Program; use crate::shared_tests::*; use crate::table::cascade_table; @@ -1155,6 +1158,7 @@ pub(crate) mod tests { use crate::table::table_column::LookupExtTableColumn::PublicEvaluationArgument; use crate::table::table_column::MasterBaseTableColumn; use crate::table::table_column::MasterExtTableColumn; + use crate::table::table_column::OpStackBaseTableColumn; use crate::table::table_column::ProcessorBaseTableColumn; use crate::table::table_column::ProcessorExtTableColumn::InputTableEvalArg; use crate::table::table_column::ProcessorExtTableColumn::OutputTableEvalArg; @@ -1247,40 +1251,19 @@ pub(crate) mod tests { let prev_instruction = row[ProcessorBaseTableColumn::PreviousInstruction.base_table_index()].value(); - let curr_instruction = row[ProcessorBaseTableColumn::CI.base_table_index()].value(); - let next_instruction_or_arg = - row[ProcessorBaseTableColumn::NIA.base_table_index()].value(); - - // sorry about this mess – this is just a test. - let pi = match AnInstruction::::try_from(prev_instruction) { + let pi = match Instruction::try_from(prev_instruction) { Ok(AnInstruction::Halt) | Err(_) => "-".to_string(), - Ok(instr) => instr.to_string().split('0').collect_vec()[0].to_owned(), - }; - let ci = AnInstruction::::try_from(curr_instruction).unwrap(); - let nia = if ci.size() == 2 { - next_instruction_or_arg.to_string() - } else { - AnInstruction::::try_from(next_instruction_or_arg) - .unwrap() - .to_string() - .split('0') - .collect_vec()[0] - .to_owned() - }; - let ci_string = if ci.size() == 1 { - ci.to_string() - } else { - ci.to_string().split('0').collect_vec()[0].to_owned() + Ok(instr) => instr.name().to_string(), }; - let interesting_cols = [clk, pi, ci_string, nia, st0, st1, st2, st3, ramp, ramv]; - println!( - "{}", - interesting_cols - .iter() - .map(|ff| format!("{:>10}", format!("{ff}"))) - .collect_vec() - .join(" | ") - ); + let (ci, nia) = ci_and_nia_from_master_table_row(row); + + let interesting_cols = [clk, pi, ci, nia, st0, st1, st2, st3, ramp, ramv]; + let interesting_cols = interesting_cols + .iter() + .map(|ff| format!("{:>10}", format!("{ff}"))) + .collect_vec() + .join(" | "); + println!("{interesting_cols}"); } println!(); println!("RAM Table:"); @@ -1295,22 +1278,128 @@ pub(crate) mod tests { let prev_instruction = row[RamBaseTableColumn::PreviousInstruction.base_table_index()].value(); - let pi = match AnInstruction::::try_from(prev_instruction) { + let pi = match Instruction::try_from(prev_instruction) { Ok(AnInstruction::Halt) | Err(_) => "-".to_string(), - Ok(instr) => instr.to_string().split('0').collect_vec()[0].to_owned(), + Ok(instr) => instr.name().to_string(), }; - let interersting_cols = [clk, pi, ramp, ramv, iord]; - println!( - "{}", - interersting_cols - .iter() - .map(|ff| format!("{:>10}", format!("{ff}"))) - .collect_vec() - .join(" | ") - ); + + let interesting_cols = [clk, pi, ramp, ramv, iord]; + let interesting_cols = interesting_cols + .iter() + .map(|ff| format!("{:>10}", format!("{ff}"))) + .collect_vec() + .join(" | "); + println!("{interesting_cols}"); + } + } + + #[test] + fn print_op_stack_table_example_for_specification() { + let num_interesting_rows = 30; + let fake_op_stack_size = 4; + + let program = triton_program! { + push 42 push 43 push 44 push 45 push 46 push 47 push 48 + nop pop pop pop pop + push 77 swap 3 push 78 swap 3 push 79 + pop pop pop pop pop pop + halt + }; + let (_, _, master_base_table) = + master_base_table_for_low_security_level(&program, [].into(), [].into()); + + println!(); + println!("Processor Table:"); + println!( + "| clk | ci | nia | st0 | st1 \ + | st2 | st3 | underflow | pointer |" + ); + println!( + "|-----------:|:-----------|-----------:|-----------:|-----------:\ + |-----------:|-----------:|:-----------|-----------:|" + ); + for row in master_base_table + .table(ProcessorTable) + .rows() + .into_iter() + .take(num_interesting_rows) + { + let clk = row[ProcessorBaseTableColumn::CLK.base_table_index()].to_string(); + let st0 = row[ProcessorBaseTableColumn::ST0.base_table_index()].to_string(); + let st1 = row[ProcessorBaseTableColumn::ST1.base_table_index()].to_string(); + let st2 = row[ProcessorBaseTableColumn::ST2.base_table_index()].to_string(); + let st3 = row[ProcessorBaseTableColumn::ST3.base_table_index()].to_string(); + let st4 = row[ProcessorBaseTableColumn::ST4.base_table_index()].to_string(); + let st5 = row[ProcessorBaseTableColumn::ST5.base_table_index()].to_string(); + let st6 = row[ProcessorBaseTableColumn::ST6.base_table_index()].to_string(); + let st7 = row[ProcessorBaseTableColumn::ST7.base_table_index()].to_string(); + let st8 = row[ProcessorBaseTableColumn::ST8.base_table_index()].to_string(); + let st9 = row[ProcessorBaseTableColumn::ST9.base_table_index()].to_string(); + + let osp = row[ProcessorBaseTableColumn::OpStackPointer.base_table_index()]; + let osp = + (osp.value() + fake_op_stack_size).saturating_sub(OpStackElement::COUNT as u64); + + let underflow_size = osp.saturating_sub(fake_op_stack_size); + let underflow_candidates = [st4, st5, st6, st7, st8, st9]; + let underflow = underflow_candidates + .into_iter() + .take(underflow_size as usize); + let underflow = underflow.map(|ff| format!("{:>2}", format!("{ff}"))); + let underflow = format!("[{}]", underflow.collect_vec().join(", ")); + + let osp = osp.to_string(); + let (ci, nia) = ci_and_nia_from_master_table_row(row); + + let interesting_cols = [clk, ci, nia, st0, st1, st2, st3, underflow, osp]; + let interesting_cols = interesting_cols + .map(|ff| format!("{:>10}", format!("{ff}"))) + .join(" | "); + println!("{interesting_cols}"); + } + + println!(); + println!("Op Stack Table:"); + println!("| clk | ib1 | pointer | value |"); + println!("|-----------:|-----------:|-----------:|-----------:|"); + for row in master_base_table + .table(TableId::OpStackTable) + .rows() + .into_iter() + .take(num_interesting_rows) + { + let clk = row[OpStackBaseTableColumn::CLK.base_table_index()].to_string(); + let ib1 = row[OpStackBaseTableColumn::IB1ShrinkStack.base_table_index()].to_string(); + + let osp = row[OpStackBaseTableColumn::StackPointer.base_table_index()]; + let osp = + (osp.value() + fake_op_stack_size).saturating_sub(OpStackElement::COUNT as u64); + let osp = osp.to_string(); + + let value = + row[OpStackBaseTableColumn::FirstUnderflowElement.base_table_index()].to_string(); + + let interesting_cols = [clk, ib1, osp, value]; + let interesting_cols = interesting_cols + .map(|ff| format!("{:>10}", format!("{ff}"))) + .join(" | "); + println!("{interesting_cols}"); } } + fn ci_and_nia_from_master_table_row(row: ArrayView1) -> (String, String) { + let curr_instruction = row[ProcessorBaseTableColumn::CI.base_table_index()].value(); + let next_instruction_or_arg = row[ProcessorBaseTableColumn::NIA.base_table_index()].value(); + + let curr_instruction = Instruction::try_from(curr_instruction).unwrap(); + let nia = if curr_instruction.has_arg() { + next_instruction_or_arg.to_string() + } else { + "".to_string() + }; + (curr_instruction.name().to_string(), nia) + } + /// To be used with `-- --nocapture`. Has mainly informative purpose. #[test] fn print_all_constraint_degrees() { From f177d6586275c56155f3a065f25a85de2ea9e54b Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Tue, 24 Oct 2023 15:44:04 +0200 Subject: [PATCH 22/27] doc: update AET relations diagram --- specification/src/img/aet-relations.ipe | 4 ++-- specification/src/img/aet-relations.png | Bin 43655 -> 43755 bytes specification/src/img/program-attestation.png | Bin 40476 -> 40559 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specification/src/img/aet-relations.ipe b/specification/src/img/aet-relations.ipe index 572890f46..9d60e4d16 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} @@ -354,7 +354,7 @@ h 236 128 c 216 128 l - + 180 124 m 180 108 l 180 104 diff --git a/specification/src/img/aet-relations.png b/specification/src/img/aet-relations.png index d810adae769cbb4a1e10a8c0c509bb2dd6ffc35e..1f415274aa653d6b86324bde9498a74eeca3e51d 100644 GIT binary patch delta 26413 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 diff --git a/specification/src/img/program-attestation.png b/specification/src/img/program-attestation.png index a872f48a34c61b0ec5f39b29c45b077121ff789a..5baf0554a3faee13b5eb5b92836786eb59e20237 100644 GIT binary patch delta 22035 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%0D31Ae+a= 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 YdDHy7Dx@#QLBM6GX96$2X3G)$Uv% Date: Tue, 24 Oct 2023 15:57:21 +0200 Subject: [PATCH 23/27] doc: Op Stack Table padding --- specification/src/operational-stack-table.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/specification/src/operational-stack-table.md b/specification/src/operational-stack-table.md index a392cb0f9..a5fdd0d43 100644 --- a/specification/src/operational-stack-table.md +++ b/specification/src/operational-stack-table.md @@ -111,11 +111,10 @@ The Op Stack Table has 2 extension columns, `rppa` and `ClockJumpDifferenceLooku ## Padding -A padding row is a direct copy of the Op Stack Table's row with the highest value for column `clk`, called template row, with the exception of the cycle count column `clk`. -In a padding row, the value of column `clk` is 1 greater than the value of column `clk` in the template row. -The padding row is inserted right below the template row. -These steps are repeated until the desired padded height is reached. -In total, above steps ensure that the Permutation Argument between the Op Stack Table and the [Processor Table](processor-table.md) holds up. +The last row in the Op Stack Table is taken as the padding template row. +Should the Op Stack Table be empty, the row (`clk`, `shrink_stack`, `stack_pointer`, `first_underflow_element`) = (0, 0, 16, 0) is used instead. +In the template row, the `shrink_stack` indicator is set to 2, signifying padding. +The template row is inserted below the last row until the desired padded height is reached. ## Memory-Consistency From 3fb003b600f92db63e58ba23881091612a97aebb Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 25 Oct 2023 10:34:30 +0200 Subject: [PATCH 24/27] doc: update Op Stack Table's AIR --- specification/src/operational-stack-table.md | 58 +++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/specification/src/operational-stack-table.md b/specification/src/operational-stack-table.md index a5fdd0d43..0b4b2f5b9 100644 --- a/specification/src/operational-stack-table.md +++ b/specification/src/operational-stack-table.md @@ -121,7 +121,7 @@ The template row is inserted below the last row until the desired padded height Memory-consistency follows from two more primitive properties: 1. Contiguity of regions of constant memory pointer. - Since the memory pointer for the Op Stack table, `osp` can change by at most one per cycle, it is possible to enforce a full sorting using AIR constraints. + Since in Op Stack Table, the `stack_pointer` can change by at most one per cycle, it is possible to enforce a full sorting using AIR constraints. 2. Correct inner-sorting within contiguous regions. Specifically, the rows within each contiguous region of constant memory pointer should be sorted for clock cycle. This property is established by the clock jump difference [Lookup Argument](lookup-argument.md). @@ -135,18 +135,16 @@ Both types of challenges are X-field elements, _i.e._, elements of $\mathbb{F}_{ ## Initial Constraints -1. `clk` is 0 -1. `osv` is 0. -1. `osp` is the number of available stack registers, _i.e._, 16. -1. The running product for the permutation argument with the Processor Table `rppa` starts off having accumulated the first row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. +1. The `stack_pointer` is the number of available stack registers, _i.e._, 16. +1. If the row is not a padding row, the running product for the permutation argument with the Processor Table `rppa` starts off having accumulated the first row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. + Otherwise, it is the Permutation Argument's default initial, _i.e._, 1. 1. The logarithmic derivative for the clock jump difference lookup `ClockJumpDifferenceLookupClientLogDerivative` is 0. ### Initial Constraints as Polynomials -1. `clk` -1. `osv` -1. `osp - 16` -1. `rppa - (🪤 - 🍋·clk - 🍊·ib1 - 🍉·osp - 🫒osv)` +1. `stack_pointer - 16` +1. `(shrink_stack - 2)·(rppa - (🪤 - 🍋·clk - 🍊·shrink_stack - 🍉·stack_pointer - 🫒·first_underflow_element))`
+ `+ (shrink_stack - 0)·(shrink_stack - 1)·(rppa - 1)` 1. `ClockJumpDifferenceLookupClientLogDerivative` ## Consistency Constraints @@ -155,30 +153,36 @@ None. ## Transition Constraints -1. - - the `osp` increases by 1, *or* - - the `osp` does not change AND the `osv` does not change, *or* - - the `osp` does not change AND the shrink stack indicator `shrink_stack` is 1. -1. The running product for the permutation argument with the Processor Table `rppa` absorbs the next row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. -1. If the op stack pointer `osp` does not change, then the logarithmic derivative for the clock jump difference lookup `ClockJumpDifferenceLookupClientLogDerivative` accumulates a factor `(clk' - clk)` relative to indeterminate 🪞. - Otherwise, it remains the same. +1. - the `stack_pointer` increases by 1, *or* + - the `stack_pointer` does not change AND the `first_underflow_element` does not change, *or* + - the `stack_pointer` does not change AND the shrink stack indicator `shrink_stack` in the next row is 0. +1. If thex next row is not a padding row, the running product for the permutation argument with the Processor Table `rppa` absorbs the next row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. + Otherwise, the running product remains unchanged. +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 and the op stack pointer `stack_pointer` does not change, then the logarithmic derivative for the clock jump difference lookup `ClockJumpDifferenceLookupClientLogDerivative` accumulates a factor `(clk' - clk)` relative to indeterminate 🪞. + Otherwise, it remains the unchanged. Written as Disjunctive Normal Form, the same constraints can be expressed as: -1. - - the `osp` increases by 1 or the `osp` does not change - - the `osp` increases by 1 or the `osv` does not change or the shrink stack indicator `shrink_stack` is 1 -1. `rppa' = rppa·(🪤 - 🍋·clk' - 🍊·ib1' - 🍉·osp' - 🫒osv')` -1. - the `osp` changes or the logarithmic derivative accumulates a summand, and - - the `osp` does not change or the logarithmic derivative does not change. +1. The `stack_pointer` increases by 1 or the `stack_pointer` does not change. +1. The `stack_pointer` increases by 1 or the `first_underflow_element` does not change or the shrink stack indicator `shrink_stack` in the next row is 0. +1. - The next row is a padding row or `rppa` has accumulated the next row, and + - the next row is not a padding row or `rppa` remains unchanged. +1. The current row is not a padding row or the next row is a padding row. +1. - the `stack_pointer` changes or the next row is a padding row or the logarithmic derivative accumulates a summand, + - the `stack_pointer` remains unchanged or the logarithmic derivative remains unchanged, and + - the next row is not a padding row or the logarithmic derivative remains unchanged. ### Transition Constraints as Polynomials -1. `(osp' - (osp + 1))·(osp' - osp)` -1. `(osp' - (osp + 1))·(osv' - osv)·(1 - shrink_stack)` -1. `rppa' - rppa·(🪤 - 🍋·clk' - 🍊·ib1' - 🍉·osp' - 🫒osv')` -1. `(osp' - (osp + 1))·((ClockJumpDifferenceLookupClientLogDerivative' - ClockJumpDifferenceLookupClientLogDerivative) · (🪞 - clk' + clk) - 1)`
- `+ (osp' - osp)·(ClockJumpDifferenceLookupClientLogDerivative' - ClockJumpDifferenceLookupClientLogDerivative)` +1. `(stack_pointer' - stack_pointer - 1)·(stack_pointer' - stack_pointer)` +1. `(stack_pointer' - stack_pointer - 1)·(first_underflow_element' - first_underflow_element)·(shrink_stack' - 0)` +1. `(shrink_stack' - 2)·(rppa' - rppa·(🪤 - 🍋·clk' - 🍊·shrink_stack' - 🍉·stack_pointer' - 🫒first_underflow_element'))`
+ `+ (shrink_stack' - 0)·(shrink_stack' - 1)·(rppa' - rppa)` +1. `(shrink_stack - 0)·(shrink_stack - 1)·(shrink_stack' - 2)` +1. `(stack_pointer' - stack_pointer - 1)·(shrink_stack' - 2)·((ClockJumpDifferenceLookupClientLogDerivative' - ClockJumpDifferenceLookupClientLogDerivative) · (🪞 - clk' + clk) - 1)`
+ `+ (stack_pointer' - stack_pointer)·(ClockJumpDifferenceLookupClientLogDerivative' - ClockJumpDifferenceLookupClientLogDerivative)`
+ `+ (shrink_stack' - 0)·(shrink_stack' - 1)·(ClockJumpDifferenceLookupClientLogDerivative' - ClockJumpDifferenceLookupClientLogDerivative)` ## Terminal Constraints From 308d67ffe41ef7e88037dec502d9f9cc07fbe31d Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 25 Oct 2023 10:53:54 +0200 Subject: [PATCH 25/27] fix: add missing transition constraint for instruction `swap` --- triton-vm/src/table/processor_table.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/triton-vm/src/table/processor_table.rs b/triton-vm/src/table/processor_table.rs index 27be66005..59356bf7d 100644 --- a/triton-vm/src/table/processor_table.rs +++ b/triton-vm/src/table/processor_table.rs @@ -1370,9 +1370,15 @@ impl ExtProcessorTable { let curr_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(CurrentBaseRow(col.master_base_table_index())) }; + let curr_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(CurrentExtRow(col.master_ext_table_index())) + }; let next_base_row = |col: ProcessorBaseTableColumn| { circuit_builder.input(NextBaseRow(col.master_base_table_index())) }; + let next_ext_row = |col: ProcessorExtTableColumn| { + circuit_builder.input(NextExtRow(col.master_ext_table_index())) + }; let specific_constraints = vec![ indicator_poly(0), @@ -1422,6 +1428,7 @@ impl ExtProcessorTable { (one() - indicator_poly(14)) * (next_base_row(ST14) - curr_base_row(ST14)), (one() - indicator_poly(15)) * (next_base_row(ST15) - curr_base_row(ST15)), next_base_row(OpStackPointer) - curr_base_row(OpStackPointer), + next_ext_row(OpStackTablePermArg) - curr_ext_row(OpStackTablePermArg), ]; [ specific_constraints, From 4bbc2d2a32101f518ef1a9bd96f5c7c8a3cfa576 Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 25 Oct 2023 11:17:26 +0200 Subject: [PATCH 26/27] fix(doc): correct explanations for previous designs --- .../src/contiguity-of-memory-pointer-regions.md | 12 +++++++++--- specification/src/registers.md | 11 +++++++---- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/specification/src/contiguity-of-memory-pointer-regions.md b/specification/src/contiguity-of-memory-pointer-regions.md index 877c5b47e..fd280b062 100644 --- a/specification/src/contiguity-of-memory-pointer-regions.md +++ b/specification/src/contiguity-of-memory-pointer-regions.md @@ -2,10 +2,12 @@ ## Contiguity for Op Stack Table -In each cycle, the memory pointer for the Op Stack Table, `osp`, can only ever increase by one, remain the same, or decrease by one. As a result, it is easy to enforce that the entire table is sorted for memory pointer using one initial constraint and one transition constraint. +In each cycle, the memory pointer for the Op Stack Table, `op_stack_pointer`, can only ever increase by one, remain the same, or decrease by one. +As a result, it is easy to enforce that the entire table is sorted for memory pointer using one initial constraint and one transition constraint. - - Initial constraint: `osp` starts with zero, so in terms of polynomials the constraint is `osp`. - - Transition constraint: the new `osp` is either the same as the previous or one larger. The polynomial representation for this constraint is `(osp' - osp - 1) * (osp' - osp)`. + - Initial constraint: `op_stack_pointer` starts with 16[^op_stack], so in terms of polynomials the constraint is `op_stack_pointer - 16`. + - Transition constraint: the new `op_stack_pointer` is either the same as the previous or one larger. + The polynomial representation for this constraint is `(op_stack_pointer' - op_stack_pointer - 1) · (op_stack_pointer' - op_stack_pointer)`. ## Contiguity for Jump Stack Table @@ -147,3 +149,7 @@ None. ### Terminal - `bc0 ⋅ rpp + bc1 ⋅ fd - 1` + +--- + +[^op_stack]: See [data structures](data-structures.md#operational-stack) and [registers](registers.md#stack) for explanations of the specific value 16. diff --git a/specification/src/registers.md b/specification/src/registers.md index 721644849..9860f061c 100644 --- a/specification/src/registers.md +++ b/specification/src/registers.md @@ -17,7 +17,7 @@ the remaining registers exist only to enable an efficient arithmetization and ar | `jso` | jump stack origin | contains the value of the instruction pointer of the last `call` | | `jsd` | jump stack destination | contains the argument of the last `call` | | `st0` through `st15` | operational stack registers | contain explicit operational stack values | -| *`osp` | operational stack pointer | contains the op stack address of the top of the operational stack | +| *`op_stack_pointer` | operational stack pointer | the current size of the operational stack | | *`osv` | operational stack value | contains the (stack) memory value at the given address | | *`hv0` through `hv6` | helper variable registers | helper variables for some arithmetic operations | | *`ramp` | RAM pointer | contains an address pointing into the RAM | @@ -40,11 +40,14 @@ In order to access elements of the op stack held in op stack underflow memory, t The stack grows upwards, in line with the metaphor that justifies the name "stack". For reasons of arithmetization, the stack always contains a minimum of 16 elements. -All these elements are initially 0. Trying to run an instruction which would result in a stack of smaller total length than 16 crashes the VM. -The registers `osp` and `osv` are not directly accessible by the program running in TritonVM. -They exist only to allow efficient arithmetization. +Stack elements `st0` through `st10` are initially 0. +Stack elements `st11` through `st15`, _i.e._, the very bottom of the stack, are initialized with the hash digest of the program that is being executed. +See [the mechanics of program attestation](program-attestation.md#mechanics) for further explanations on stack initialization. + +The register `op_stack_pointer` is not directly accessible by the program running in TritonVM. +It exists only to allow efficient arithmetization. ## RAM From e59eedebcaa25b977ea80892e7e6227ac27caeef Mon Sep 17 00:00:00 2001 From: Jan Ferdinand Sauer Date: Wed, 25 Oct 2023 11:18:44 +0200 Subject: [PATCH 27/27] doc: update Processor Table's AET and AIR --- specification/src/instruction-groups.md | 24 +++++++++---------- ...ruction-specific-transition-constraints.md | 6 ++--- specification/src/processor-table.md | 12 ++++------ specification/src/registers.md | 1 - 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/specification/src/instruction-groups.md b/specification/src/instruction-groups.md index 5f7ec4a86..72429fa6b 100644 --- a/specification/src/instruction-groups.md +++ b/specification/src/instruction-groups.md @@ -197,8 +197,8 @@ Contains all constraints from instruction group `keep_jump_stack`, and additiona 1. The stack element in `st12` is moved into `st13`. 1. The stack element in `st13` is moved into `st14`. 1. The stack element in `st14` is moved into `st15`. -1. The stack element in `st15` is moved to the top of op stack underflow, i.e., `osv`. 1. The op stack pointer is incremented by 1. +1. The running product for the Op Stack Table absorbs the current row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. ### Polynomials @@ -216,8 +216,8 @@ Contains all constraints from instruction group `keep_jump_stack`, and additiona 1. `st13' - st12` 1. `st14' - st13` 1. `st15' - st14` -1. `osv' - st15` -1. `osp' - (osp + 1)` +1. `op_stack_pointer' - (op_stack_pointer + 1)` +1. `RunningProductOpStackTable' - RunningProductOpStackTable·(🪤 - 🍋·clk - 🍊·ib1 - 🍉·op_stack_pointer - 🫒·st15)` ## Group `grow_stack` @@ -240,8 +240,8 @@ Contains all constraints from instruction group `stack_grows_and_top_2_unconstra 1. The stack element in `st13` does not change. 1. The stack element in `st14` does not change. 1. The stack element in `st15` does not change. -1. The top of the op stack underflow, i.e., `osv`, does not change. 1. The op stack pointer does not change. +1. The running product for the Op Stack Table remains unchanged. ### Polynomials @@ -250,8 +250,8 @@ Contains all constraints from instruction group `stack_grows_and_top_2_unconstra 1. `st13' - st13` 1. `st14' - st14` 1. `st15' - st15` -1. `osv' - osv` -1. `osp' - osp` +1. `op_stack_pointer' - op_stack_pointer` +1. `RunningProductOpStackTable' - RunningProductOpStackTable` ## Group `stack_remains_and_top_10_unconstrained` @@ -317,7 +317,7 @@ Contains all constraints from instruction group `unary_operation`, and additiona ## Group `stack_shrinks_and_top_3_unconstrained` -This instruction group requires helper variable `hv0` to hold the multiplicative inverse of `(osp - 16)`. +This instruction group requires helper variable `hv0` to hold the multiplicative inverse of `(op_stack_pointer - 16)`. In effect, this means that the op stack pointer can only be decremented if it is not 16, i.e., if op stack underflow memory is not empty. Since the stack can only change by one element at a time, this prevents stack underflow. @@ -335,9 +335,9 @@ Since the stack can only change by one element at a time, this prevents stack un 1. The stack element in `st13` is moved into `st12`. 1. The stack element in `st14` is moved into `st13`. 1. The stack element in `st15` is moved into `st14`. -1. The stack element at the top of op stack underflow, i.e., `osv`, is moved into `st15`. 1. The op stack pointer is decremented by 1. -1. The helper variable register `hv0` holds the inverse of `(osp - 16)`. +1. The helper variable register `hv0` holds the inverse of `(op_stack_pointer - 16)`. +1. The running product for the Op Stack Table absorbs clock cycle and instruction bit 1 from the current row as well as op stack pointer and stack element 15 from the next row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. ### Polynomials @@ -353,9 +353,9 @@ Since the stack can only change by one element at a time, this prevents stack un 1. `st12' - st13` 1. `st13' - st14` 1. `st14' - st15` -1. `st15' - osv` -1. `osp' - (osp - 1)` -1. `(osp - 16)·hv0 - 1` +1. `op_stack_pointer' - (op_stack_pointer - 1)` +1. `(op_stack_pointer - 16)·hv0 - 1` +1. `RunningProductOpStackTable' - RunningProductOpStackTable·(🪤 - 🍋·clk - 🍊·ib1 - 🍉·op_stack_pointer' - 🫒·st15')` ## Group `binary_operation` diff --git a/specification/src/instruction-specific-transition-constraints.md b/specification/src/instruction-specific-transition-constraints.md index a222a4684..d7a8fb708 100644 --- a/specification/src/instruction-specific-transition-constraints.md +++ b/specification/src/instruction-specific-transition-constraints.md @@ -134,8 +134,8 @@ Additionally, it defines the following transition constraints. 1. If `i` is not 13, then `st13` does not change. 1. If `i` is not 14, then `st14` does not change. 1. If `i` is not 15, then `st15` does not change. -1. The top of the op stack underflow, i.e., `osv`, does not change. 1. The op stack pointer does not change. +1. The running product for the Op Stack Table remains unchanged. ### Polynomials @@ -185,8 +185,8 @@ Additionally, it defines the following transition constraints. 1. `(1 - ind_13(hv3, hv2, hv1, hv0))·(st13' - st13)` 1. `(1 - ind_14(hv3, hv2, hv1, hv0))·(st14' - st14)` 1. `(1 - ind_15(hv3, hv2, hv1, hv0))·(st15' - st15)` -1. `osv' - osv` -1. `osp' - osp` +1. `op_stack_pointer' - op_stack_pointer` +1. `RunningProductOpStackTable' - RunningProductOpStackTable` ### Helper variable definitions for `swap` + `i` diff --git a/specification/src/processor-table.md b/specification/src/processor-table.md index 1de3b4ac7..2cf7136c7 100644 --- a/specification/src/processor-table.md +++ b/specification/src/processor-table.md @@ -99,13 +99,12 @@ However, in order to verify the correctness of `RunningEvaluationHashDigest`, th 1. The operational stack element `st10` is 0. 1. The [Evaluation Argument](evaluation-argument.md) of operational stack elements `st11` through `st15` with respect to indeterminate 🥬 equals the public part of program digest challenge, 🫑. See [program attestation](program-attestation.md) for more details. -1. The operational stack pointer `osp` is 16. -1. The operational stack value `osv` is 0. +1. The `op_stack_pointer` is 16. 1. The RAM pointer `ramp` is 0. 1. `RunningEvaluationStandardInput` is 1. 1. `RunningEvaluationStandardOutput` is 1. 1. `InstructionLookupClientLogDerivative` has absorbed the first row with respect to challenges 🥝, 🥥, and 🫐 and indeterminate 🪥. -1. `RunningProductOpStackTable` has absorbed the first row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. +1. `RunningProductOpStackTable` is 1. 1. `RunningProductRamTable` has absorbed the first row with respect to challenges 🍍, 🍈, 🍎, and 🌽 and indeterminate 🛋. 1. `RunningProductJumpStackTable` has absorbed the first row with respect to challenges 🍇, 🍅, 🍌, 🍏, and 🍐 and indeterminate 🧴. 1. `RunningEvaluationHashInput` has absorbed the first row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪 if the current instruction is `hash`. Otherwise, it is 1. @@ -134,13 +133,12 @@ See [program attestation](program-attestation.md) for more details. 1. `st9` 1. `st10` 1. `🥬^5 + st11·🥬^4 + st12·🥬^3 + st13·🥬^2 + st14·🥬 + st15 - 🫑` -1. `osp` -1. `osv` +1. `op_stack_pointer - 16` 1. `ramp` 1. `RunningEvaluationStandardInput - 1` 1. `RunningEvaluationStandardOutput - 1` 1. `InstructionLookupClientLogDerivative · (🪥 - 🥝·ip - 🥥·ci - 🫐·nia) - 1` -1. `RunningProductOpStackTable - (🪤 - 🍋·clk - 🍊·ib1 - 🍉·osp - 🫒·osv)` +1. `RunningProductOpStackTable - 1` 1. `RunningProductRamTable - (🛋 - 🍍·clk - 🍈·ramp - 🍎·ramv - 🌽·previous_instruction)` 1. `RunningProductJumpStackTable - (🧴 - 🍇·clk - 🍅·ci - 🍌·jsp - 🍏·jso - 🍐·jsd)` 1. `(ci - opcode(hash))·(RunningEvaluationHashInput - 1)`
@@ -189,7 +187,6 @@ The following additional constraints also apply to every pair of rows. 1. The running evaluation for standard input absorbs `st0` of the next row with respect to 🛏 if the current instruction is `read_io`, and remains unchanged otherwise. 1. The running evaluation for standard output absorbs `st0` of the next row with respect to 🧯 if the current instruction in the next row is `write_io`, and remains unchanged otherwise. 1. If the next row is not a padding row, the logarithmic derivative for the Program Table absorbs the next row with respect to challenges 🥝, 🥥, and 🫐 and indeterminate 🪥. Otherwise, it remains unchanged. -1. The running product for the Op Stack Table absorbs the next row with respect to challenges 🍋, 🍊, 🍉, and 🫒 and indeterminate 🪤. 1. The running product for the RAM Table absorbs the next row with respect to challenges 🍍, 🍈, 🍎, and 🌽 and indeterminate 🛋. 1. The running product for the Jump Stack Table absorbs the next row with respect to challenges 🍇, 🍅, 🍌, 🍏, and 🍐 and indeterminate 🧴. 1. If the current instruction in the next row is `hash`, the running evaluation “Hash Input” absorbs the next row with respect to challenges 🧄₀ through 🧄₉ and indeterminate 🚪. Otherwise, it remains unchanged. @@ -218,7 +215,6 @@ Otherwise, the running evaluation remains unchanged. `+ write_io_deselector'·(RunningEvaluationStandardOutput' - 🧯·RunningEvaluationStandardOutput - st0')` 1. `(1 - IsPadding') · ((InstructionLookupClientLogDerivative' - InstructionLookupClientLogDerivative) · (🛁 - 🥝·ip' - 🥥·ci' - 🫐·nia') - 1)`
`+ IsPadding'·(RunningProductInstructionTable' - RunningProductInstructionTable)` -1. `RunningProductOpStackTable' - RunningProductOpStackTable·(🪤 - 🍋·clk' - 🍊·ib1' - 🍉·osp' - 🫒·osv')` 1. `RunningProductRamTable' - RunningProductRamTable·(🛋 - 🍍·clk' - 🍈·ramp' - 🍎·ramv' - 🌽·previous_instruction')` 1. `RunningProductJumpStackTable' - RunningProductJumpStackTable·(🧴 - 🍇·clk' - 🍅·ci' - 🍌·jsp' - 🍏·jso' - 🍐·jsd')` 1. `(ci' - opcode(hash))·(RunningEvaluationHashInput' - RunningEvaluationHashInput)`
diff --git a/specification/src/registers.md b/specification/src/registers.md index 9860f061c..00e5fd6e3 100644 --- a/specification/src/registers.md +++ b/specification/src/registers.md @@ -18,7 +18,6 @@ the remaining registers exist only to enable an efficient arithmetization and ar | `jsd` | jump stack destination | contains the argument of the last `call` | | `st0` through `st15` | operational stack registers | contain explicit operational stack values | | *`op_stack_pointer` | operational stack pointer | the current size of the operational stack | -| *`osv` | operational stack value | contains the (stack) memory value at the given address | | *`hv0` through `hv6` | helper variable registers | helper variables for some arithmetic operations | | *`ramp` | RAM pointer | contains an address pointing into the RAM | | *`ramv` | RAM value | contains the value of the RAM element at the address currently held in `ramp` |