diff --git a/CHANGELOG.md b/CHANGELOG.md index a6fd8ff734..5025d3d5f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ #### Upcoming Changes +* feat: implement an opcode that computes QM31 arithmetics (add, sub, mul, div) in the VM [#1938](https://github.com/lambdaclass/cairo-vm/pull/1938) + * feat: add functions that compute packed reduced qm31 arithmetics to `math_utils` [#1944](https://github.com/lambdaclass/cairo-vm/pull/1944) * feat: implement `Blake2sLastBlock` opcode in VM [#1932](https://github.com/lambdaclass/cairo-vm/pull/1932) diff --git a/cairo_programs/stwo_exclusive_programs/qm31_opcodes_test.cairo b/cairo_programs/stwo_exclusive_programs/qm31_opcodes_test.cairo new file mode 100644 index 0000000000..4d791ebc58 --- /dev/null +++ b/cairo_programs/stwo_exclusive_programs/qm31_opcodes_test.cairo @@ -0,0 +1,183 @@ +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.bool import FALSE, TRUE + +// Tests the QM31_add_mul opcode runners using specific examples as reference. +// The test is comprised of 10 test cases, each of which tests a different combination of missing_operand, is_imm, and is_mul. +// is_mul determines whether the operation is a multiplication or an addition of QM31 elements. +// is_imm determines whether op1 is an immediate. +// missing_operand determines which operand is missing and needs to be computed by the VM (0 for dst, 1 for op0, 2 fo op1). +// the combination of is_imm=TRUE with missig_operand=2 is not tested because we do not use arithmetic opcodes to deduce immediates. +func main{}() { + let qm31_op0_coordinates_a = 0x544b2fba; + let qm31_op0_coordinates_b = 0x673cff77; + let qm31_op0_coordinates_c = 0x60713d44; + let qm31_op0_coordinates_d = 0x499602d2; + let qm31_op0 = qm31_op0_coordinates_a + qm31_op0_coordinates_b*(2**36) + qm31_op0_coordinates_c*(2**72) + qm31_op0_coordinates_d*(2**108); + + let qm31_op1_coordinates_a = 0x4b18de99; + let qm31_op1_coordinates_b = 0x55f6fb62; + let qm31_op1_coordinates_c = 0x6e2290d9; + let qm31_op1_coordinates_d = 0x7cd851b9; + let qm31_op1 = qm31_op1_coordinates_a + qm31_op1_coordinates_b*(2**36) + qm31_op1_coordinates_c*(2**72) + qm31_op1_coordinates_d*(2**108); + + let qm31_add_dst_coordinates_a = 0x1f640e54; + let qm31_add_dst_coordinates_b = 0x3d33fada; + let qm31_add_dst_coordinates_c = 0x4e93ce1e; + let qm31_add_dst_coordinates_d = 0x466e548c; + let qm31_add_dst = qm31_add_dst_coordinates_a + qm31_add_dst_coordinates_b*(2**36) + qm31_add_dst_coordinates_c*(2**72) + qm31_add_dst_coordinates_d*(2**108); + + let qm31_mul_dst_coordinates_a = 0x38810ab4; + let qm31_mul_dst_coordinates_b = 0x5a0fd30a; + let qm31_mul_dst_coordinates_c = 0x2527b81e; + let qm31_mul_dst_coordinates_d = 0x4b1ed1cd; + let qm31_mul_dst = qm31_mul_dst_coordinates_a + qm31_mul_dst_coordinates_b*(2**36) + qm31_mul_dst_coordinates_c*(2**72) + qm31_mul_dst_coordinates_d*(2**108); + + let runner_output_mul_dst = run_qm31_operation(missing_operand=0, is_imm=FALSE, is_mul=TRUE, dst_or_op0=qm31_op0, op0_or_op1=qm31_op1); + assert runner_output_mul_dst = qm31_mul_dst; + let runner_output_add_dst = run_qm31_operation(missing_operand=0, is_imm=FALSE, is_mul=FALSE, dst_or_op0=qm31_op0, op0_or_op1=qm31_op1); + assert runner_output_add_dst = qm31_add_dst; + + let runner_output_mul_op0 = run_qm31_operation(missing_operand=1, is_imm=FALSE, is_mul=TRUE, dst_or_op0=qm31_mul_dst, op0_or_op1=qm31_op1); + assert runner_output_mul_op0 = qm31_op0; + let runner_output_add_op0 = run_qm31_operation(missing_operand=1, is_imm=FALSE, is_mul=FALSE, dst_or_op0=qm31_add_dst, op0_or_op1=qm31_op1); + assert runner_output_add_op0 = qm31_op0; + + let runner_output_mul_op1 = run_qm31_operation(missing_operand=2, is_imm=FALSE, is_mul=TRUE, dst_or_op0=qm31_mul_dst, op0_or_op1=qm31_op0); + assert runner_output_mul_op1 = qm31_op1; + let runner_output_add_op1 = run_qm31_operation(missing_operand=2, is_imm=FALSE, is_mul=FALSE, dst_or_op0=qm31_add_dst, op0_or_op1=qm31_op0); + assert runner_output_add_op1 = qm31_op1; + + let runner_output_mul_dst = run_qm31_operation(missing_operand=0, is_imm=TRUE, is_mul=TRUE, dst_or_op0=qm31_op0, op0_or_op1=qm31_op1); + assert runner_output_mul_dst = qm31_mul_dst; + let runner_output_add_dst = run_qm31_operation(missing_operand=0, is_imm=TRUE, is_mul=FALSE, dst_or_op0=qm31_op0, op0_or_op1=qm31_op1); + assert runner_output_add_dst = qm31_add_dst; + + let runner_output_mul_op0 = run_qm31_operation(missing_operand=1, is_imm=TRUE, is_mul=TRUE, dst_or_op0=qm31_mul_dst, op0_or_op1=qm31_op1); + assert runner_output_mul_op0 = qm31_op0; + let runner_output_add_op0 = run_qm31_operation(missing_operand=1, is_imm=TRUE, is_mul=FALSE, dst_or_op0=qm31_add_dst, op0_or_op1=qm31_op1); + assert runner_output_add_op0 = qm31_op0; + + return (); +} + +// Forces the runner to execute the QM31_add_mul opcode with the given operands. +// missing_operand, is_imm, is_mul determine the configuration of the operation as described above. +// dst_or_op0 is a felt representing the value of either the op0 (if missing_operand=0) or dst (otherwise) operand. +// op0_or_op1 is a felt representing the value of either the op0 (if missing_operand=2) or op1 (otherwise) operand. +// dst_or_op0 and op0_or_op1 are stored within addresses fp-4 and fp-3 respectively, they are passed to the instruction +// using offsets wrt fp (unless is_imm=TRUE, in which case op1 has offset 1 relative to pc). +// The missing operand has offset 0 relative to ap. +// An instruction encoding with the appropriate flags and offsets is built, then written to [pc] and the runner is forced to execute QM31_add_mul. +// The missing operand is deduced to [ap] and returned. +func run_qm31_operation( + missing_operand: felt, + is_imm: felt, + is_mul: felt, + dst_or_op0: felt, + op0_or_op1: felt, +) -> felt { + alloc_locals; + + // Set flags and offsets. + let (local offsets) = alloc(); + let (local flags) = alloc(); + + assert offsets[missing_operand] = 2**15; // the missing operand will be written to [ap] + + assert flags[2] = is_imm; // flag_op1_imm = 0; + assert flags[5] = 1-is_mul; // flag_res_add = 1-is_mul; + assert flags[6] = is_mul; // flag_res_mul = is_mul; + assert flags[7] = 0; // flag_PC_update_jump = 0; + assert flags[8] = 0; // flag_PC_update_jump_rel = 0; + assert flags[9] = 0; // flag_PC_update_jnz = 0; + assert flags[10] = 0; // flag_ap_update_add = 0; + assert flags[11] = 0; // flag_ap_update_add_1 = 0; + assert flags[12] = 0; // flag_opcode_call = 0; + assert flags[13] = 0; // flag_opcode_ret = 0; + assert flags[14] = 1; // flag_opcode_assert_eq = 1; + + if (missing_operand == 0) { + assert offsets[1] = 2**15 - 4; + assert offsets[2] = 2**15 - 3 + 4 * is_imm; + assert flags[0] = 0; // flag_dst_base_fp + assert flags[1] = 1; // flag_op0_base_fp + } + if (missing_operand == 1) { + assert offsets[0] = 2**15 - 4; + assert offsets[2] = 2**15 - 3 + 4 * is_imm; + assert flags[0] = 1; // flag_dst_base_fp + assert flags[1] = 0; // flag_op0_base_fp + } + if (missing_operand == 2) { + assert is_imm = FALSE; + assert offsets[0] = 2**15 - 4; + assert offsets[1] = 2**15 - 3; + assert flags[0] = 1; // flag_dst_base_fp + assert flags[1] = 1; // flag_op0_base_fp + } + assert flags[3] = (2 - flags[0] - flags[1]) * (1 - is_imm); // flag_op1_base_fp + assert flags[4] = 1 - is_imm - flags[3]; // flag_op1_base_ap + + // Compute the instruction encoding. + let flag_num = flags[0] + flags[1]*(2**1) + flags[2]*(2**2) + flags[3]*(2**3) + flags[4]*(2**4) + flags[5]*(2**5) + flags[6]*(2**6) + flags[14]*(2**14); + let qm31_opcode_extension_num = 3; + let instruction_encoding = offsets[0] + offsets[1]*(2**16) + offsets[2]*(2**32) + flag_num*(2**48) + qm31_opcode_extension_num*(2**63); + + // Run the instruction and return the result. + if (is_imm == TRUE) { + assert op0_or_op1 = 0x7cd851b906e2290d9055f6fb6204b18de99; + if (missing_operand == 0) { + if (is_mul == TRUE) { + assert instruction_encoding=0x1c04680017ffc8000; + dw 0x1c04680017ffc8000; + dw 0x7cd851b906e2290d9055f6fb6204b18de99; + return [ap]; + } + assert instruction_encoding=0x1c02680017ffc8000; + dw 0x1c02680017ffc8000; + dw 0x7cd851b906e2290d9055f6fb6204b18de99; + return [ap]; + } + if (missing_operand == 1) { + if (is_mul == TRUE) { + assert instruction_encoding=0x1c045800180007ffc; + dw 0x1c045800180007ffc; + dw 0x7cd851b906e2290d9055f6fb6204b18de99; + return [ap]; + } + assert instruction_encoding=0x1c025800180007ffc; + dw 0x1c025800180007ffc; + dw 0x7cd851b906e2290d9055f6fb6204b18de99; + return [ap]; + } + } + + if (missing_operand == 0) { + if (is_mul == TRUE) { + assert instruction_encoding=0x1c04a7ffd7ffc8000; + dw 0x1c04a7ffd7ffc8000; + return [ap]; + } + assert instruction_encoding=0x1c02a7ffd7ffc8000; + dw 0x1c02a7ffd7ffc8000; + return [ap]; + } + if (missing_operand == 1) { + if (is_mul == TRUE) { + assert instruction_encoding=0x1c0497ffd80007ffc; + dw 0x1c0497ffd80007ffc; + return [ap]; + } + assert instruction_encoding=0x1c0297ffd80007ffc; + dw 0x1c0297ffd80007ffc; + return [ap]; + } + if (is_mul == TRUE) { + assert instruction_encoding=0x1c05380007ffd7ffc; + dw 0x1c05380007ffd7ffc; + return [ap]; + } + assert instruction_encoding=0x1c03380007ffd7ffc; + dw 0x1c03380007ffd7ffc; + return [ap]; +} diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 22798e3f97..425a5796fd 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -61,6 +61,7 @@ pub mod hint_processor; pub mod math_utils; pub mod program_hash; pub mod serde; +pub mod typed_operations; pub mod types; pub mod utils; pub mod vm; diff --git a/vm/src/math_utils/mod.rs b/vm/src/math_utils/mod.rs index 85ca438f40..565a768b7c 100644 --- a/vm/src/math_utils/mod.rs +++ b/vm/src/math_utils/mod.rs @@ -24,7 +24,7 @@ lazy_static! { .collect::>(); } -const STWO_PRIME: u64 = (1 << 31) - 1; +pub const STWO_PRIME: u64 = (1 << 31) - 1; const STWO_PRIME_U128: u128 = STWO_PRIME as u128; const MASK_36: u64 = (1 << 36) - 1; const MASK_8: u64 = (1 << 8) - 1; @@ -101,7 +101,7 @@ fn qm31_packed_reduced_read_coordinates(felt: Felt252) -> Result<[u64; 4], MathE /// Reduces four u64 coordinates and packs them into a single Felt252. /// STWO_PRIME fits in 36 bits, hence each coordinate can be represented by 36 bits and a QM31 /// element can be stored in the first 144 bits of a Felt252. -fn qm31_coordinates_to_packed_reduced(coordinates: [u64; 4]) -> Felt252 { +pub fn qm31_coordinates_to_packed_reduced(coordinates: [u64; 4]) -> Felt252 { let bytes_part1 = ((coordinates[0] % STWO_PRIME) as u128 + (((coordinates[1] % STWO_PRIME) as u128) << 36)) .to_le_bytes(); diff --git a/vm/src/tests/cairo_run_test.rs b/vm/src/tests/cairo_run_test.rs index cbce327c08..01524cc38c 100644 --- a/vm/src/tests/cairo_run_test.rs +++ b/vm/src/tests/cairo_run_test.rs @@ -576,6 +576,14 @@ fn blake2s_opcode_test() { run_program_simple(program_data.as_slice()); } +#[test] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] +fn qm31_opcodes_test() { + let program_data = + include_bytes!("../../../cairo_programs/stwo_exclusive_programs/qm31_opcodes_test.json"); + run_program_simple(program_data.as_slice()); +} + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn relocate_segments() { diff --git a/vm/src/typed_operations.rs b/vm/src/typed_operations.rs new file mode 100644 index 0000000000..4802160824 --- /dev/null +++ b/vm/src/typed_operations.rs @@ -0,0 +1,172 @@ +use crate::math_utils::{ + qm31_packed_reduced_add, qm31_packed_reduced_div, qm31_packed_reduced_mul, + qm31_packed_reduced_sub, +}; +use crate::stdlib::prelude::*; +use crate::types::relocatable::MaybeRelocatable; +use crate::types::{errors::math_errors::MathError, instruction::OpcodeExtension}; +use crate::vm::errors::vm_errors::VirtualMachineError; +use crate::Felt252; + +/// Adds two MaybeRelocatable values according to the specified OpcodeExtension and returns the +/// result as a MaybeRelocatable value. +/// If the OpcodeExtension is Stone it adds them as MaybeRelocatable::add does. +/// If the OpcodeExtension is QM31Operation it requires them both to be Int and it adds them +/// as packed reduced QM31 elements. +pub fn typed_add( + x: &MaybeRelocatable, + y: &MaybeRelocatable, + opcode_extension: OpcodeExtension, +) -> Result { + match opcode_extension { + OpcodeExtension::Stone => Ok(x.add(y)?), + OpcodeExtension::QM31Operation => { + if let (MaybeRelocatable::Int(num_x), MaybeRelocatable::Int(num_y)) = (x, y) { + Ok(MaybeRelocatable::Int(qm31_packed_reduced_add( + *num_x, *num_y, + )?)) + } else { + Err(VirtualMachineError::Math(MathError::RelocatableQM31Add( + Box::new((x.clone(), y.clone())), + ))) + } + } + _ => Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension( + "typed_add".to_owned().into_boxed_str(), + )), + } +} + +/// Substracts two MaybeRelocatable values according to the specified OpcodeExtension and returns +/// the result as a MaybeRelocatable value. +/// If the OpcodeExtension is Stone it subtracts them as MaybeRelocatable::sub does. +/// If the OpcodeExtension is QM31Operation it requires them both to be Int and it subtracts +/// them as packed reduced QM31 elements. +pub fn typed_sub( + x: &MaybeRelocatable, + y: &MaybeRelocatable, + opcode_extension: OpcodeExtension, +) -> Result { + match opcode_extension { + OpcodeExtension::Stone => Ok(x.sub(y)?), + OpcodeExtension::QM31Operation => { + if let (MaybeRelocatable::Int(num_x), MaybeRelocatable::Int(num_y)) = (x, y) { + Ok(MaybeRelocatable::Int(qm31_packed_reduced_sub( + *num_x, *num_y, + )?)) + } else { + Err(VirtualMachineError::Math(MathError::RelocatableQM31Sub( + Box::new((x.clone(), y.clone())), + ))) + } + } + _ => Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension( + "typed_sub".to_owned().into_boxed_str(), + )), + } +} + +/// Multiplies two MaybeRelocatable values according to the specified OpcodeExtension and returns +/// the result as a MaybeRelocatable value. +/// Requires both operands to be Int. +/// If the OpcodeExtension is Stone it multiplies them as Felts. +/// If the OpcodeExtension is QM31Operation it multiplies them as packed reduced QM31 elements. +pub fn typed_mul( + x: &MaybeRelocatable, + y: &MaybeRelocatable, + opcode_extension: OpcodeExtension, +) -> Result { + if let (MaybeRelocatable::Int(num_x), MaybeRelocatable::Int(num_y)) = (x, y) { + match opcode_extension { + OpcodeExtension::Stone => Ok(MaybeRelocatable::Int(num_x * num_y)), + OpcodeExtension::QM31Operation => Ok(MaybeRelocatable::Int(qm31_packed_reduced_mul( + *num_x, *num_y, + )?)), + _ => Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension( + "typed_mul".to_owned().into_boxed_str(), + )), + } + } else { + Err(VirtualMachineError::ComputeResRelocatableMul(Box::new(( + x.clone(), + y.clone(), + )))) + } +} + +/// Divides two Felt252 values according to the specified OpcodeExtension and returns the result +/// as a Felt252 value. +/// If the OpcodeExtension is Stone it divides them as Felts. +/// If the OpcodeExtension is QM31Operation it divides them as packed reduced QM31 elements. +pub fn typed_div( + x: &Felt252, + y: &Felt252, + opcode_extension: OpcodeExtension, +) -> Result { + match opcode_extension { + OpcodeExtension::Stone => { + Ok(x.field_div(&y.try_into().map_err(|_| MathError::DividedByZero)?)) + } + OpcodeExtension::QM31Operation => Ok(qm31_packed_reduced_div(*x, *y)?), + _ => Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension( + "typed_div".to_owned().into_boxed_str(), + )), + } +} +#[cfg(test)] +mod decoder_test { + use super::*; + use assert_matches::assert_matches; + + #[cfg(target_arch = "wasm32")] + use wasm_bindgen_test::*; + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn typed_add_blake() { + let a = &MaybeRelocatable::from(5); + let b = &MaybeRelocatable::from(6); + let error = typed_add(a, b, OpcodeExtension::Blake); + assert_matches!( + error, + Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension(ref message)) if message.as_ref() == "typed_add" + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn typed_sub_blake() { + let a = &MaybeRelocatable::from(7); + let b = &MaybeRelocatable::from(3); + let error = typed_sub(a, b, OpcodeExtension::Blake); + assert_matches!( + error, + Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension(ref message)) if message.as_ref() == "typed_sub" + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn relocatable_typed_sub_q31_operation() { + let a = &MaybeRelocatable::from((6, 8)); + let b = &MaybeRelocatable::from(2); + let error = typed_sub(a, b, OpcodeExtension::QM31Operation); + assert_matches!( + error, + Err(VirtualMachineError::Math(MathError::RelocatableQM31Sub(bx))) if *bx == + (MaybeRelocatable::from((6, 8)), MaybeRelocatable::from(2)) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn typed_mul_blake_finalize() { + let a = &MaybeRelocatable::from(4); + let b = &MaybeRelocatable::from(9); + let error = typed_mul(a, b, OpcodeExtension::BlakeFinalize); + assert_matches!( + error, + Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension(ref message)) if message.as_ref() == "typed_mul" + ); + } +} diff --git a/vm/src/types/errors/math_errors.rs b/vm/src/types/errors/math_errors.rs index ac5631cf3a..1db1df947b 100644 --- a/vm/src/types/errors/math_errors.rs +++ b/vm/src/types/errors/math_errors.rs @@ -42,6 +42,10 @@ pub enum MathError { RelocatableAddUsizeOffsetExceeded(Box<(Relocatable, usize)>), #[error("Operation failed: {} + {}, can't add two relocatable values", (*.0).0, (*.0).1)] RelocatableAdd(Box<(Relocatable, Relocatable)>), + #[error("Operation failed: {} - {}, can't add a relocatable value as a QM31 element", (*.0).0, (*.0).1)] + RelocatableQM31Add(Box<(MaybeRelocatable, MaybeRelocatable)>), + #[error("Operation failed: {} - {}, can't subtract a relocatable value or from a relocatable value as a QM31 element", (*.0).0, (*.0).1)] + RelocatableQM31Sub(Box<(MaybeRelocatable, MaybeRelocatable)>), #[error("Operation failed: {} - {}, can't subtract two relocatable values with different segment indexes", (*.0).0, (*.0).1)] RelocatableSubDiffIndex(Box<(Relocatable, Relocatable)>), #[error( diff --git a/vm/src/types/instruction.rs b/vm/src/types/instruction.rs index 439130b83c..9a8da5f299 100644 --- a/vm/src/types/instruction.rs +++ b/vm/src/types/instruction.rs @@ -82,6 +82,7 @@ pub enum OpcodeExtension { Stone, Blake, BlakeFinalize, + QM31Operation, } impl Instruction { diff --git a/vm/src/vm/decoding/decoder.rs b/vm/src/vm/decoding/decoder.rs index 85fa3e0495..9dd02ea276 100644 --- a/vm/src/vm/decoding/decoder.rs +++ b/vm/src/vm/decoding/decoder.rs @@ -104,6 +104,7 @@ pub fn decode_instruction(encoded_instr: u128) -> Result OpcodeExtension::Stone, 1 => OpcodeExtension::Blake, 2 => OpcodeExtension::BlakeFinalize, + 3 => OpcodeExtension::QM31Operation, _ => { return Err(VirtualMachineError::InvalidOpcodeExtension( opcode_extension_num, @@ -111,19 +112,29 @@ pub fn decode_instruction(encoded_instr: u128) -> Result ApUpdate::Add2, (0, false) => ApUpdate::Regular, @@ -479,9 +490,46 @@ mod decoder_test { // opcode_extension| opcode|ap_update|pc_update|res_logic|op1_src|op0_reg|dst_reg // 79 ... 17 16 15| 14 13 12| 11 10| 9 8 7| 6 5|4 3 2| 1| 0 // ???| CALL| Add2| JumpRel| Op1| IMM| FP| FP - // 0 1 1 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 - // 0001 1001 0001 0000 0100 = 0x39104; off0 = 0, off1 = 1 - let error = decode_instruction(0x19104800180018000); - assert_matches!(error, Err(VirtualMachineError::InvalidOpcodeExtension(3))); + // 1 1 1 0 0 1 0 0 0 1 0 0 0 0 0 1 0 0 + // 0011 1001 0001 0000 0100 = 0x39104; off0 = 0, off1 = 1 + let error = decode_instruction(0x39104800180018000); + assert_matches!(error, Err(VirtualMachineError::InvalidOpcodeExtension(7))); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn decode_qm31_operation_invalid_flags() { + // opcode_extension| opcode|ap_update|pc_update|res_logic|op1_src|op0_reg|dst_reg + // 79 ... 17 16 15| 14 13 12| 11 10| 9 8 7| 6 5|4 3 2| 1| 0 + // QM31Operation| CALL| REGULAR| JumpRel| Op1| FP| AP| AP + // 1 1 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 + // 1 1001 0001 0000 1000 = 0x19108; off0 = 1, off1 = 1 + let error = decode_instruction(0x19108800180018001); + assert_matches!( + error, + Err(VirtualMachineError::InvalidQM31AddMulFlags(0x1108)) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn decode_qm31_operation() { + // opcode_extension| opcode|ap_update|pc_update|res_logic|op1_src|op0_reg|dst_reg + // 79 ... 17 16 15| 14 13 12| 11 10| 9 8 7| 6 5|4 3 2| 1| 0 + // QM31Operation|ASSERT_EQ| REGULAR| REGULAR| MUL| FP| AP| AP + // 1 1 1 0 0 0 0 0 0 0 1 0 0 1 0 0 0 + // 1 1100 0000 0100 1000 = 0x1c048; off0 = 1, off1 = 1 + let inst = decode_instruction(0x1c048800180018001).unwrap(); + assert_matches!(inst.opcode, Opcode::AssertEq); + assert_matches!(inst.off0, 1); + assert_matches!(inst.off1, 1); + assert_matches!(inst.dst_register, Register::AP); + assert_matches!(inst.op0_register, Register::AP); + assert_matches!(inst.op1_addr, Op1Addr::FP); + assert_matches!(inst.res, Res::Mul); + assert_matches!(inst.pc_update, PcUpdate::Regular); + assert_matches!(inst.ap_update, ApUpdate::Regular); + assert_matches!(inst.fp_update, FpUpdate::Regular); + assert_matches!(inst.opcode_extension, OpcodeExtension::QM31Operation); } } diff --git a/vm/src/vm/errors/vm_errors.rs b/vm/src/vm/errors/vm_errors.rs index 9162d591bd..971d73e1e7 100644 --- a/vm/src/vm/errors/vm_errors.rs +++ b/vm/src/vm/errors/vm_errors.rs @@ -60,6 +60,10 @@ pub enum VirtualMachineError { "Failed to compute Res.MUL: Could not complete computation of non pure values {} * {}", (*.0).0, (*.0).1 )] ComputeResRelocatableMul(Box<(MaybeRelocatable, MaybeRelocatable)>), + #[error( + "Failed to compute operand, attempted to use {0} for an OpcodeExtension that is neither Stone nor QM31Operation" + )] + InvalidTypedOperationOpcodeExtension(Box), #[error("Couldn't compute operand {}. Unknown value for memory cell {}", (*.0).0, (*.0).1)] FailedToComputeOperands(Box<(String, Relocatable)>), #[error("An ASSERT_EQ instruction failed: {} != {}.", (*.0).0, (*.0).1)] @@ -140,6 +144,8 @@ pub enum VirtualMachineError { Blake2sInvalidOperand(u8, u8), #[error("Blake2s opcode invalid flags {0}")] InvalidBlake2sFlags(u128), + #[error("QM31 add mul opcode invalid flags {0}")] + InvalidQM31AddMulFlags(u128), } #[cfg(test)] diff --git a/vm/src/vm/vm_core.rs b/vm/src/vm/vm_core.rs index 87d9998138..447f9c85d0 100644 --- a/vm/src/vm/vm_core.rs +++ b/vm/src/vm/vm_core.rs @@ -1,7 +1,6 @@ use crate::math_utils::signed_felt; use crate::stdlib::{any::Any, borrow::Cow, collections::HashMap, prelude::*}; use crate::types::builtin_name::BuiltinName; -use crate::types::instruction::OpcodeExtension; #[cfg(feature = "extensive_hints")] use crate::types::program::HintRange; use crate::{ @@ -9,11 +8,13 @@ use crate::{ builtin_hint_processor::blake2s_hash::blake2s_compress, hint_processor_definition::HintProcessor, }, + typed_operations::{typed_add, typed_div, typed_mul, typed_sub}, types::{ errors::math_errors::MathError, exec_scope::ExecutionScopes, instruction::{ - is_call_instruction, ApUpdate, FpUpdate, Instruction, Opcode, PcUpdate, Res, + is_call_instruction, ApUpdate, FpUpdate, Instruction, Opcode, OpcodeExtension, + PcUpdate, Res, }, relocatable::{MaybeRelocatable, Relocatable}, }, @@ -237,19 +238,18 @@ impl VirtualMachine { None, )), Opcode::AssertEq => match (&instruction.res, dst, op1) { - (Res::Add, Some(dst_addr), Some(op1_addr)) => { - Ok((Some(dst_addr.sub(op1_addr)?), dst.cloned())) - } + (Res::Add, Some(dst_addr), Some(op1_addr)) => Ok(( + Some(typed_sub(dst_addr, op1_addr, instruction.opcode_extension)?), + dst.cloned(), + )), ( Res::Mul, Some(MaybeRelocatable::Int(num_dst)), Some(MaybeRelocatable::Int(num_op1)), - ) if !num_op1.is_zero() => Ok(( - Some(MaybeRelocatable::Int(num_dst.field_div( - &num_op1.try_into().map_err(|_| MathError::DividedByZero)?, - ))), - dst.cloned(), - )), + ) if !num_op1.is_zero() => { + let num_op0 = typed_div(num_dst, num_op1, instruction.opcode_extension)?; + Ok((Some(MaybeRelocatable::Int(num_op0)), dst.cloned())) + } _ => Ok((None, None)), }, _ => Ok((None, None)), @@ -270,7 +270,9 @@ impl VirtualMachine { Res::Op1 => return Ok((dst.cloned(), dst.cloned())), Res::Add => { return Ok(( - dst.zip(op0).and_then(|(dst, op0)| dst.sub(&op0).ok()), + dst.zip(op0).and_then(|(dst, op0)| { + typed_sub(dst, &op0, instruction.opcode_extension).ok() + }), dst.cloned(), )) } @@ -279,12 +281,8 @@ impl VirtualMachine { Some(MaybeRelocatable::Int(num_dst)), Some(MaybeRelocatable::Int(num_op0)), ) if !num_op0.is_zero() => { - return Ok(( - Some(MaybeRelocatable::Int(num_dst.field_div( - &num_op0.try_into().map_err(|_| MathError::DividedByZero)?, - ))), - dst.cloned(), - )) + let num_op1 = typed_div(num_dst, &num_op0, instruction.opcode_extension)?; + return Ok((Some(MaybeRelocatable::Int(num_op1)), dst.cloned())); } _ => (), }, @@ -318,17 +316,8 @@ impl VirtualMachine { ) -> Result, VirtualMachineError> { match instruction.res { Res::Op1 => Ok(Some(op1.clone())), - Res::Add => Ok(Some(op0.add(op1)?)), - Res::Mul => { - if let (MaybeRelocatable::Int(num_op0), MaybeRelocatable::Int(num_op1)) = (op0, op1) - { - return Ok(Some(MaybeRelocatable::Int(num_op0 * num_op1))); - } - Err(VirtualMachineError::ComputeResRelocatableMul(Box::new(( - op0.clone(), - op1.clone(), - )))) - } + Res::Add => Ok(Some(typed_add(op0, op1, instruction.opcode_extension)?)), + Res::Mul => Ok(Some(typed_mul(op0, op1, instruction.opcode_extension)?)), Res::Unconstrained => Ok(None), } } @@ -1339,6 +1328,7 @@ impl VirtualMachineBuilder { mod tests { use super::*; use crate::felt_hex; + use crate::math_utils::{qm31_coordinates_to_packed_reduced, STWO_PRIME}; use crate::stdlib::collections::HashMap; use crate::types::instruction::OpcodeExtension; use crate::types::layout_name::LayoutName; @@ -2295,6 +2285,106 @@ mod tests { ); } + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn deduce_op0_qm31_add_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Add, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op1_coordinates = [STWO_PRIME - 10, 5, STWO_PRIME - 5, 1]; + let dst_coordinates = [STWO_PRIME - 4, 2, 12, 3]; + let op1_packed = qm31_coordinates_to_packed_reduced(op1_coordinates); + let dst_packed = qm31_coordinates_to_packed_reduced(dst_coordinates); + let op1 = MaybeRelocatable::Int(op1_packed); + let dst = MaybeRelocatable::Int(dst_packed); + assert_matches!( + vm.deduce_op0(&instruction, Some(&dst), Some(&op1)), + Ok::<(Option, Option), VirtualMachineError>(( + x, + y + )) if x == Some(MaybeRelocatable::Int(qm31_coordinates_to_packed_reduced([6, STWO_PRIME-3, 17, 2]))) && + y == Some(MaybeRelocatable::Int(dst_packed)) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn deduce_op0_qm31_mul_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Mul, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op1_coordinates = [0, 0, 1, 0]; + let dst_coordinates = [0, 0, 0, 1]; + let op1_packed = qm31_coordinates_to_packed_reduced(op1_coordinates); + let dst_packed = qm31_coordinates_to_packed_reduced(dst_coordinates); + let op1 = MaybeRelocatable::Int(op1_packed); + let dst = MaybeRelocatable::Int(dst_packed); + assert_matches!( + vm.deduce_op0(&instruction, Some(&dst), Some(&op1)), + Ok::<(Option, Option), VirtualMachineError>(( + x, + y + )) if x == Some(MaybeRelocatable::Int(qm31_coordinates_to_packed_reduced([0, 1, 0, 0]))) && + y == Some(MaybeRelocatable::Int(dst_packed)) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn deduce_op0_blake_finalize_add_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Add, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::BlakeFinalize, + }; + + let vm = vm!(); + + let op1 = MaybeRelocatable::Int(Felt252::from(5)); + let dst = MaybeRelocatable::Int(Felt252::from(15)); + assert_matches!( + vm.deduce_op0(&instruction, Some(&dst), Some(&op1)), + Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension(ref message)) if message.as_ref() == "typed_sub" + ); + } + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn deduce_op1_opcode_call() { @@ -2504,6 +2594,106 @@ mod tests { ); } + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn deduce_op1_qm31_add_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Add, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op0_coordinates = [4, STWO_PRIME - 13, 3, 7]; + let dst_coordinates = [8, 7, 6, 5]; + let op0_packed = qm31_coordinates_to_packed_reduced(op0_coordinates); + let dst_packed = qm31_coordinates_to_packed_reduced(dst_coordinates); + let op0 = MaybeRelocatable::Int(op0_packed); + let dst = MaybeRelocatable::Int(dst_packed); + assert_matches!( + vm.deduce_op1(&instruction, Some(&dst), Some(op0)), + Ok::<(Option, Option), VirtualMachineError>(( + x, + y + )) if x == Some(MaybeRelocatable::Int(qm31_coordinates_to_packed_reduced([4, 20, 3, STWO_PRIME - 2]))) && + y == Some(MaybeRelocatable::Int(dst_packed)) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn deduce_op1_qm31_mul_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Mul, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op0_coordinates = [0, 1, 0, 0]; + let dst_coordinates = [STWO_PRIME - 1, 0, 0, 0]; + let op0_packed = qm31_coordinates_to_packed_reduced(op0_coordinates); + let dst_packed = qm31_coordinates_to_packed_reduced(dst_coordinates); + let op0 = MaybeRelocatable::Int(op0_packed); + let dst = MaybeRelocatable::Int(dst_packed); + assert_matches!( + vm.deduce_op1(&instruction, Some(&dst), Some(op0)), + Ok::<(Option, Option), VirtualMachineError>(( + x, + y + )) if x == Some(MaybeRelocatable::Int(qm31_coordinates_to_packed_reduced([0, 1, 0, 0]))) && + y == Some(MaybeRelocatable::Int(dst_packed)) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn deduce_op1_blake_mul_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Mul, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::Blake, + }; + + let vm = vm!(); + + let op0 = MaybeRelocatable::Int(Felt252::from(4)); + let dst = MaybeRelocatable::Int(Felt252::from(16)); + assert_matches!( + vm.deduce_op1(&instruction, Some(&dst), Some(op0)), + Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension(ref message)) if message.as_ref() == "typed_div" + ); + } + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn compute_res_op1() { @@ -2622,6 +2812,130 @@ mod tests { ); } + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn compute_res_qm31_add_relocatable_values() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Add, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op1 = MaybeRelocatable::from((2, 3)); + let op0 = MaybeRelocatable::from(7); + assert_matches!( + vm.compute_res(&instruction, &op0, &op1), + Err(VirtualMachineError::Math(MathError::RelocatableQM31Add(bx))) if *bx == (op0, op1) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn compute_res_qm31_add_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Add, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op1_coordinates = [1, 2, 3, 4]; + let op0_coordinates = [10, 11, STWO_PRIME - 1, 13]; + let op1_packed = qm31_coordinates_to_packed_reduced(op1_coordinates); + let op0_packed = qm31_coordinates_to_packed_reduced(op0_coordinates); + let op1 = MaybeRelocatable::Int(op1_packed); + let op0 = MaybeRelocatable::Int(op0_packed); + assert_matches!( + vm.compute_res(&instruction, &op0, &op1), + Ok::, VirtualMachineError>(Some(MaybeRelocatable::Int( + x + ))) if x == qm31_coordinates_to_packed_reduced([11, 13, 2, 17]) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn compute_res_qm31_mul_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Mul, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::QM31Operation, + }; + + let vm = vm!(); + + let op1_coordinates = [0, 0, 1, 0]; + let op0_coordinates = [0, 0, 1, 0]; + let op1_packed = qm31_coordinates_to_packed_reduced(op1_coordinates); + let op0_packed = qm31_coordinates_to_packed_reduced(op0_coordinates); + let op1 = MaybeRelocatable::Int(op1_packed); + let op0 = MaybeRelocatable::Int(op0_packed); + assert_matches!( + vm.compute_res(&instruction, &op0, &op1), + Ok::, VirtualMachineError>(Some(MaybeRelocatable::Int( + x + ))) if x == qm31_coordinates_to_packed_reduced([2, 1, 0, 0]) + ); + } + + #[test] + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + fn compute_res_blake_mul_int_operands() { + let instruction = Instruction { + off0: 1, + off1: 2, + off2: 3, + dst_register: Register::FP, + op0_register: Register::AP, + op1_addr: Op1Addr::AP, + res: Res::Mul, + pc_update: PcUpdate::Regular, + ap_update: ApUpdate::Regular, + fp_update: FpUpdate::Regular, + opcode: Opcode::AssertEq, + opcode_extension: OpcodeExtension::Blake, + }; + + let vm = vm!(); + + let op1 = MaybeRelocatable::Int(Felt252::from(11)); + let op0 = MaybeRelocatable::Int(Felt252::from(12)); + assert_matches!( + vm.compute_res(&instruction, &op0, &op1), + Err(VirtualMachineError::InvalidTypedOperationOpcodeExtension(ref message)) if message.as_ref() == "typed_mul" + ); + } + #[test] #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] fn compute_res_unconstrained() {