diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a19ade077d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 53ab9a6de2..0a834aedd5 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -42,6 +42,5 @@ jobs: auto-push: true alert-threshold: '130%' comment-on-alert: true - alert-comment-cc-users: '@unbalancedparentheses' - name: Clean benches run: make clean diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ade377346e..fb92c4e12b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,6 +18,7 @@ env: cairo_programs/**/*.json !cairo_programs/manually_compiled/* cairo_programs/cairo-1-programs/bitwise.sierra + tests_cairo/**/*.json TEST_COLLECT_COVERAGE: 1 PROPTEST_CASES: 100 @@ -48,6 +49,7 @@ jobs: - cairo_test_programs - cairo_1_test_contracts - cairo_2_test_contracts + - tests_cairo_programs name: Build Cairo programs runs-on: ubuntu-24.04 steps: @@ -66,7 +68,7 @@ jobs: id: cache-programs with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} # This is not pretty, but we need `make` to see the compiled programs are # actually newer than the sources, otherwise it will try to rebuild them @@ -120,37 +122,43 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_test_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_test_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch proof programs uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch bench programs uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_bench_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_bench_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch test contracts (Cairo 1) uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_1_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_1_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch test contracts (Cairo 2) uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_2_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_2_test_contracts-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} + fail-on-cache-miss: true + - name: Fetch tests_cairo programs + uses: actions/cache/restore@v3 + with: + path: ${{ env.CAIRO_PROGRAMS_PATH }} + key: tests_cairo_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Merge caches uses: actions/cache/save@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} lint: needs: merge-caches @@ -179,7 +187,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Run clippy @@ -229,7 +237,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true # NOTE: we do this separately because --workspace operates in weird ways @@ -274,7 +282,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Check all features (workspace) @@ -309,7 +317,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: all-programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Install testing tools @@ -390,7 +398,7 @@ jobs: cairo_programs/**/*.air_public_input cairo_programs/**/*.air_private_input cairo_programs/**/*.pie.zip - key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} - name: Install uv if: steps.trace-cache.outputs.cache-hit != 'true' @@ -409,7 +417,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true # This is not pretty, but we need `make` to see the compiled programs are @@ -456,7 +464,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Generate traces @@ -615,7 +623,7 @@ jobs: cairo_programs/**/*.air_public_input cairo_programs/**/*.air_private_input cairo_programs/**/*.pie.zip - key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: ${{ matrix.program-target }}-reference-trace-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch traces for cairo-vm @@ -675,7 +683,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Run script @@ -715,7 +723,7 @@ jobs: uses: actions/cache/restore@v3 with: path: ${{ env.CAIRO_PROGRAMS_PATH }} - key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'Makefile', 'requirements.txt') }} + key: cairo_proof_programs-cache-${{ hashFiles('cairo_programs/**/*.cairo', 'tests_cairo/**/*.cairo', 'Makefile', 'requirements.txt') }} fail-on-cache-miss: true - name: Fetch pie diff --git a/CHANGELOG.md b/CHANGELOG.md index cd0637f22f..d53e411dc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,21 @@ Both branches support Stwo prover opcodes (Blake2s, QM31) since v2.0.0. #### Upcoming Changes +* Add Stwo cairo runner API [#2351](https://github.com/lambdaclass/cairo-vm/pull/2351) + +* Add union merge strategy for CHANGELOG.md [#2345](https://github.com/lambdaclass/cairo-vm/pull/2345) + * fix: Fix off-by-one comparisons in `split_int`, `assert_250_bit`, and `sqrt` hints [#2348](https://github.com/lambdaclass/cairo-vm/pull/2348) +* chore: Add `CairoFunctionRunner` for running Cairo entrypoints by name or PC, and broaden `CairoArg`/`MaybeRelocatable` conversions to support primitive signed/unsigned integers and big integers [#2352](https://github.com/lambdaclass/cairo-vm/pull/2352) + +* chore: Add unit tests for `CairoFunctionRunner`, `CairoArg` conversions/macros, and `MaybeRelocatable` conversion macro coverage [#2354](https://github.com/lambdaclass/cairo-vm/pull/2354) + +* test: Add `tests_cairo` utilities and a math Cairo test suite, including `assert_mr_eq!` and structured error helpers [#2353](https://github.com/lambdaclass/cairo-vm/pull/2353) + +* chore: Add `math_cmp_test` for Cairo file `math_cmp` and add `sub_mod_prime` function to `math_test_utils` [#2355](https://github.com/lambdaclass/cairo-vm/pull/2355) + + #### [3.2.0] - 2026-3-3 * fix: Change extended_resource_counter entry from u32 to usize [#2349](https://github.com/lambdaclass/cairo-vm/pull/2349) diff --git a/Makefile b/Makefile index 1734325614..5fd8556faa 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ UNAME := $(shell uname) hyper-threading-benchmarks \ cairo_bench_programs cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts \ cairo_trace cairo-vm_trace cairo_proof_trace cairo-vm_proof_trace python-deps python-deps-macos \ - build-cairo-lang hint-accountant \ create-proof-programs-symlinks \ + build-cairo-lang hint-accountant \ create-proof-programs-symlinks tests_cairo_programs \ $(RELBIN) $(DBGBIN) # Proof mode consumes too much memory with cairo-lang to execute @@ -268,6 +268,21 @@ run: check: cargo check +# ====================== +# Tests Cairo Programs +# ====================== + +TESTS_CAIRO_DIR=tests_cairo +TESTS_CAIRO_FILES:=$(shell find $(TESTS_CAIRO_DIR) -name "*.cairo") +COMPILED_TESTS_CAIRO:=$(patsubst %.cairo, %.json, $(TESTS_CAIRO_FILES)) + +$(TESTS_CAIRO_DIR)/%.json: $(TESTS_CAIRO_DIR)/%.cairo + . cairo-vm-env/bin/activate && cairo-compile \ + --cairo_path="$(TESTS_CAIRO_DIR)" \ + $< --output $@ + +tests_cairo_programs: $(COMPILED_TESTS_CAIRO) + cairo_test_programs: $(COMPILED_TESTS) $(COMPILED_BAD_TESTS) $(COMPILED_NORETROCOMPAT_TESTS) $(COMPILED_PRINT_TESTS) $(COMPILED_MOD_BUILTIN_TESTS) $(COMPILED_SECP_CAIRO0_HINTS) $(COMPILED_KZG_DA_CAIRO0_HINTS) $(COMPILED_SEGMENT_ARENA_CAIRO0_HINTS) cairo_proof_programs: $(COMPILED_PROOF_TESTS) $(COMPILED_MOD_BUILTIN_PROOF_TESTS) $(COMPILED_STWO_EXCLUSIVE_TESTS) cairo_bench_programs: $(COMPILED_BENCHES) @@ -286,7 +301,7 @@ ifdef TEST_COLLECT_COVERAGE TEST_COMMAND:=cargo llvm-cov nextest --no-report endif -test: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts cairo_1_program +test: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_2_test_contracts cairo_1_program tests_cairo_programs $(TEST_COMMAND) --workspace --features "test_utils, cairo-1-hints" test-extensive_hints: cairo_proof_programs cairo_test_programs cairo_1_test_contracts cairo_1_program cairo_2_test_contracts $(TEST_COMMAND) --workspace --features "test_utils, cairo-1-hints, cairo-0-secp-hints, cairo-0-data-availability-hints, extensive_hints" diff --git a/README.md b/README.md index 1d7dbe9764..bda09bbedb 100644 --- a/README.md +++ b/README.md @@ -246,19 +246,6 @@ When running a Cairo program directly using the Cairo-vm repository you would fi cairo_runner.initialize_segments(None); ``` -When using cairo-vm with the Starknet devnet there are additional parameters that are part of the OS context passed on to the `run_from_entrypoint` method that we do not have here when using it directly. These parameters are, for example, initial stacks of the builtins, which are the base of each of them and are needed as they are the implicit arguments of the function. - -```rust - let _var = cairo_runner.run_from_entrypoint( - entrypoint, - vec![ - &MaybeRelocatable::from(2).into(), //this is the entry point selector - &MaybeRelocatable::from((2,0)).into() //this would be the output_ptr for example if our cairo function uses it - ], - false, - &mut hint_processor, - ); -``` ### Running cairo 1 programs To run a cairo 1 program enter in the folder `cd cairo1-run` and follow the [`cairo1-run documentation`](cairo1-run/README.md) diff --git a/tests_cairo/error_utils.rs b/tests_cairo/error_utils.rs new file mode 100644 index 0000000000..0851b0d34d --- /dev/null +++ b/tests_cairo/error_utils.rs @@ -0,0 +1,170 @@ +//! Test utilities for Cairo VM result assertions. + +use cairo_vm::vm::errors::{ + cairo_run_errors::CairoRunError, hint_errors::HintError, vm_errors::VirtualMachineError, + vm_exception::VmException, +}; + +/// Asserts VM result is `Ok` or matches an error pattern. +#[macro_export] +macro_rules! assert_vm_result { + ($res:expr, ok $(,)?) => {{ + match &$res { + Ok(_) => {} + Err(e) => panic!("Expected Ok, got Err: {:#?}", e), + } + }}; + + ($res:expr, err $pat:pat $(,)?) => {{ + match &$res { + Ok(v) => panic!("Expected Err, got Ok: {v:?}"), + Err(e) => assert!( + matches!(e, $pat), + "Unexpected error variant.\nExpected: {}\nGot: {:#?}", + stringify!($pat), + e + ), + } + }}; + + ($res:expr, err $pat:pat if $guard:expr $(,)?) => {{ + match &$res { + Ok(v) => panic!("Expected Err, got Ok: {v:?}"), + Err(e) => assert!( + matches!(e, $pat if $guard), + "Unexpected error variant.\nExpected: {} (with guard)\nGot: {:#?}", + stringify!($pat), + e + ), + } + }}; +} + +/// Type alias for check functions that validate test results. +pub type VmCheck = fn(&std::result::Result); + +/// Asserts that the result is `Ok`. +pub fn expect_ok(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!(res, ok); +} + +/// Asserts that the result is `HintError::AssertNotZero`. +pub fn expect_hint_assert_not_zero(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertNotZero(_))) + ); +} + +/// Asserts that the result is `HintError::AssertNotEqualFail`. +pub fn expect_assert_not_equal_fail(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertNotEqualFail(_))) + ); +} + +/// Asserts that the result is `HintError::Internal(VirtualMachineError::DiffTypeComparison)`. +pub fn expect_diff_type_comparison(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::Internal(VirtualMachineError::DiffTypeComparison(_)))) + ); +} + +/// Asserts that the result is `HintError::Internal(VirtualMachineError::DiffIndexComp)`. +pub fn expect_diff_index_comp(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::Internal(VirtualMachineError::DiffIndexComp(_)))) + ); +} + +/// Asserts that the result is `HintError::ValueOutside250BitRange`. +pub fn expect_hint_value_outside_250_bit_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::ValueOutside250BitRange(_))) + ); +} + +/// Asserts that the result is `HintError::NonLeFelt252`. +pub fn expect_non_le_felt252(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::NonLeFelt252(_))) + ); +} + +/// Asserts that the result is `HintError::AssertLtFelt252`. +pub fn expect_assert_lt_felt252(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::AssertLtFelt252(_))) + ); +} + +/// Asserts that the result is `HintError::ValueOutsideValidRange`. +pub fn expect_hint_value_outside_valid_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::ValueOutsideValidRange(_))) + ); +} + +/// Asserts that the result is `HintError::OutOfValidRange`. +pub fn expect_hint_out_of_valid_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::OutOfValidRange(_))) + ); +} + +/// Asserts that the result is `HintError::SplitIntNotZero`. +pub fn expect_split_int_not_zero(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::SplitIntNotZero)) + ); +} + +/// Asserts that the result is `HintError::SplitIntLimbOutOfRange`. +pub fn expect_split_int_limb_out_of_range(res: &std::result::Result<(), CairoRunError>) { + assert_vm_result!( + res, + err CairoRunError::VmException(VmException { + inner_exc: VirtualMachineError::Hint(boxed), + .. + }) if matches!(boxed.as_ref(), (_, HintError::SplitIntLimbOutOfRange(_))) + ); +} diff --git a/tests_cairo/math/main_math_cmp_test.cairo b/tests_cairo/math/main_math_cmp_test.cairo new file mode 100644 index 0000000000..7fec0a00cb --- /dev/null +++ b/tests_cairo/math/main_math_cmp_test.cairo @@ -0,0 +1,12 @@ +from starkware.cairo.common.math_cmp import ( + is_not_zero, + is_nn, + is_le, + is_nn_le, + is_in_range, + is_le_felt, +) + +func main() { +return (); +} diff --git a/tests_cairo/math/main_math_test.cairo b/tests_cairo/math/main_math_test.cairo new file mode 100644 index 0000000000..c8979c82be --- /dev/null +++ b/tests_cairo/math/main_math_test.cairo @@ -0,0 +1,28 @@ +from starkware.cairo.common.math import ( + assert_not_zero, + assert_not_equal, + assert_nn, + assert_le, + assert_lt, + assert_nn_le, + assert_in_range, + assert_250_bit, + split_felt, + assert_le_felt, + assert_lt_felt, + abs_value, + sign, + unsigned_div_rem, + signed_div_rem, + split_int, + sqrt, + horner_eval, + is_quad_residue, + safe_div, + safe_mult, + assert_is_power_of_2, +) + +func main() { +return (); +} diff --git a/tests_cairo/math/math_cmp_test.rs b/tests_cairo/math/math_cmp_test.rs new file mode 100644 index 0000000000..ede2fe3e1e --- /dev/null +++ b/tests_cairo/math/math_cmp_test.rs @@ -0,0 +1,288 @@ +//! Tests for `math_cmp.cairo`. + +use std::sync::LazyLock; +use cairo_vm::types::program::Program; + +use super::math_test_utils::{sub_mod_prime, RC_BOUND}; +use cairo_vm::cairo_args; +use cairo_vm::types::builtin_name::BuiltinName; +use cairo_vm::utils::CAIRO_PRIME; +use cairo_vm::vm::runners::cairo_function_runner::CairoFunctionRunner; +use num_bigint::{BigUint, RandBigInt}; +use num_traits::{One, Zero}; +use rand::thread_rng; +use rstest::{fixture, rstest}; + +// ===================== Shared constants (LazyLock) ===================== + +/// The compiled Cairo math_cmp program, loaded once and shared across all tests. +static PROGRAM: LazyLock = + LazyLock::new(|| load_cairo_program!("main_math_cmp_test.json")); + +/// Interesting felt values used in test_is_le_felt. +static INTERESTING_FELTS: LazyLock> = LazyLock::new(|| { + let p = &*CAIRO_PRIME; + vec![ + BigUint::zero(), + BigUint::one(), + &*RC_BOUND - BigUint::one(), + RC_BOUND.clone(), + &*RC_BOUND + BigUint::one(), + BigUint::from(2u64).pow(251) - BigUint::one(), + BigUint::from(2u64).pow(251), + BigUint::from(2u64).pow(251) + BigUint::one(), + p - BigUint::from(2u64), + p - BigUint::one(), + ] +}); + +/// Pair examples used in is_le / is_nn_le / is_in_range tests. +static PAIR_EXAMPLES: LazyLock> = LazyLock::new(|| { + vec![ + (BigUint::from(0u64), BigUint::from(0u64)), + (BigUint::from(0u64), BigUint::from(1u64)), + (BigUint::from(1u64), BigUint::from(0u64)), + (BigUint::from(2u64).pow(200), BigUint::from(2u64).pow(200)), + ( + BigUint::from(2u64).pow(200), + BigUint::from(2u64).pow(200) + &*RC_BOUND - BigUint::one(), + ), + ( + BigUint::from(2u64).pow(200), + BigUint::from(2u64).pow(200) + &*RC_BOUND, + ), + ] +}); + +// ===================== Fixture ===================== + +/// Creates a fresh CairoFunctionRunner from the shared PROGRAM. +#[fixture] +fn runner() -> CairoFunctionRunner { + CairoFunctionRunner::new(&PROGRAM).unwrap() +} + +// ===================== test_is_not_zero ===================== + +#[rstest] +// Case: value=0 +// Expected: returns 0 (false). +#[case(Some(BigUint::zero()), 0i64)] +// Case: value=random (non-zero) +// Expected: returns 1 (true). +#[case::random(None, 1i64)] +fn test_is_not_zero( + mut runner: CairoFunctionRunner, + #[case] value: Option, + #[case] expected_res: i64, +) { + let value = value.unwrap_or_else(|| { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), &CAIRO_PRIME) + }); + + let args = cairo_args!(value); + runner.run_default_cairo0("is_not_zero", &args).unwrap(); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!(&ret[0], expected_res); +} + +// ===================== test_is_le_felt ===================== + +#[rstest] +fn test_is_le_felt( + mut runner: CairoFunctionRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let expected_res = if value0 <= value1 { 1i64 } else { 0i64 }; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base, value0, value1); + runner.run_default_cairo0("is_le_felt", &args).unwrap(); + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[1], expected_res); +} + +// ===================== test_is_nn ===================== + +#[rstest] +// Case: value=0 +// Expected: returns 1 (true, 0 is non-negative). +#[case::zero(BigUint::from(0u64))] +// Case: value=1 +// Expected: returns 1 (true). +#[case::one(BigUint::from(1u64))] +// Case: value=100 +// Expected: returns 1 (true). +#[case::hundred(BigUint::from(100u64))] +// Case: value=RC_BOUND-1 +// Expected: returns 1 (true, last valid non-negative value). +#[case::rc_minus_one(&*RC_BOUND - BigUint::one())] +// Case: value=RC_BOUND +// Expected: returns 0 (false, at boundary). +#[case::rc(RC_BOUND.clone())] +// Case: value=PRIME/2-1 +// Expected: returns 0 (false, too large). +#[case::prime_half_minus_one((&*CAIRO_PRIME / BigUint::from(2u64)) - BigUint::one())] +// Case: value=PRIME/2 +// Expected: returns 0 (false, too large). +#[case::prime_half(&*CAIRO_PRIME / BigUint::from(2u64))] +// Case: value=PRIME/2+1 +// Expected: returns 0 (false, too large). +#[case::prime_half_plus_one((&*CAIRO_PRIME / BigUint::from(2u64)) + BigUint::one())] +// Case: value=PRIME-10 +// Expected: returns 0 (false, near prime). +#[case::prime_minus_ten(&*CAIRO_PRIME - BigUint::from(10u64))] +// Case: value=PRIME-1 +// Expected: returns 0 (false, near prime). +#[case::prime_minus_one(&*CAIRO_PRIME - BigUint::one())] +fn test_is_nn(mut runner: CairoFunctionRunner, #[case] value: BigUint) { + let expected_res = if value < *RC_BOUND { 1i64 } else { 0i64 }; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base, value); + runner.run_default_cairo0("is_nn", &args).unwrap(); + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[1], expected_res); +} + +// ===================== test_is_le ===================== +// Tests is_le(a, b) which returns 1 if (b - a) % PRIME < RC_BOUND, else 0. + +#[rstest] +// Case: pair_0 = (0, 0) +// Expected: returns 1 (0 <= 0 in modular sense). +#[case::pair_0(0)] +// Case: pair_1 = (0, 1) +// Expected: returns 1 (0 <= 1). +#[case::pair_1(1)] +// Case: pair_2 = (1, 0) +// Expected: returns 0 (1 > 0, diff wraps around). +#[case::pair_2(2)] +// Case: pair_3 = (2^200, 2^200) +// Expected: returns 1 (equal values). +#[case::pair_3(3)] +// Case: pair_4 = (2^200, 2^200 + RC_BOUND - 1) +// Expected: returns 1 (diff < RC_BOUND). +#[case::pair_4(4)] +// Case: pair_5 = (2^200, 2^200 + RC_BOUND) +// Expected: returns 0 (diff = RC_BOUND, not less than). +#[case::pair_5(5)] +fn test_is_le(mut runner: CairoFunctionRunner, #[case] pair_idx: usize) { + let (value0, value1) = &PAIR_EXAMPLES[pair_idx]; + // diff = (value1 - value0) % PRIME + let diff = sub_mod_prime(value0, value1); + let expected_res = if diff < *RC_BOUND { 1i64 } else { 0i64 }; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base, value0, value1); + runner.run_default_cairo0("is_le", &args).unwrap(); + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[1], expected_res); +} + +// ===================== test_is_nn_le ===================== +// Tests is_nn_le(a, b) which returns 1 if 0 <= a <= b < RC_BOUND, else 0. + +#[rstest] +// Case: pair_0 = (0, 0) +// Expected: returns 0 (0 is not < RC_BOUND in this context, but 0 <= 0 < RC_BOUND is true). +#[case::pair_0(0)] +// Case: pair_1 = (0, 1) +// Expected: returns 0 (1 < RC_BOUND, 0 <= 1). +#[case::pair_1(1)] +// Case: pair_2 = (1, 0) +// Expected: returns 0 (1 > 0, fails a <= b). +#[case::pair_2(2)] +// Case: pair_3 = (2^200, 2^200) +// Expected: returns 0 (2^200 >= RC_BOUND). +#[case::pair_3(3)] +// Case: pair_4 = (2^200, 2^200 + RC_BOUND - 1) +// Expected: returns 0 (values >= RC_BOUND). +#[case::pair_4(4)] +// Case: pair_5 = (2^200, 2^200 + RC_BOUND) +// Expected: returns 0 (values >= RC_BOUND). +#[case::pair_5(5)] +fn test_is_nn_le(mut runner: CairoFunctionRunner, #[case] pair_idx: usize) { + let (value0, value1) = &PAIR_EXAMPLES[pair_idx]; + let expected_res = if value0 <= value1 && value1 < &*RC_BOUND { + 1i64 + } else { + 0i64 + }; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base, value0, value1); + runner.run_default_cairo0("is_nn_le", &args).unwrap(); + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[1], expected_res); +} + +// ===================== test_is_in_range ===================== +// Tests is_in_range(value, lower, upper) which returns 1 if: +// (value - lower) % PRIME < RC_BOUND AND (upper - 1 - value) % PRIME < RC_BOUND +// This checks if value is in the range [lower, upper) in modular arithmetic. + +#[rstest] +// Case: pair_0 = (0, 0) with various shifts +#[case::pair_0(0)] +// Case: pair_1 = (0, 1) with various shifts +#[case::pair_1(1)] +// Case: pair_2 = (1, 0) with various shifts +#[case::pair_2(2)] +// Case: pair_3 = (2^200, 2^200) with various shifts +#[case::pair_3(3)] +// Case: pair_4 = (2^200, 2^200 + RC_BOUND - 1) with various shifts +#[case::pair_4(4)] +// Case: pair_5 = (2^200, 2^200 + RC_BOUND) with various shifts +#[case::pair_5(5)] +fn test_is_in_range( + mut runner: CairoFunctionRunner, + #[case] pair_idx: usize, + // shift values: 0, RC_BOUND, 2^200, PRIME-10 + #[values( + BigUint::zero(), + RC_BOUND.clone(), + BigUint::from(2u64).pow(200), + &*CAIRO_PRIME - BigUint::from(10u64), + )] + shift: BigUint, +) { + let lower = &shift; + let value = (&PAIR_EXAMPLES[pair_idx].0 + &shift) % &*CAIRO_PRIME; + let upper = (&PAIR_EXAMPLES[pair_idx].1 + &shift) % &*CAIRO_PRIME; + let value_plus_one = &value + BigUint::one(); + // Check: (value - lower) % PRIME < RC_BOUND AND (upper - (value + 1)) % PRIME < RC_BOUND + let expected_res = if sub_mod_prime(lower, &value) < *RC_BOUND + && sub_mod_prime(&value_plus_one, &upper) < *RC_BOUND + { + 1i64 + } else { + 0i64 + }; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base, value, lower, upper); + runner.run_default_cairo0("is_in_range", &args).unwrap(); + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[1], expected_res); +} diff --git a/tests_cairo/math/math_test.rs b/tests_cairo/math/math_test.rs new file mode 100644 index 0000000000..06a6a2b944 --- /dev/null +++ b/tests_cairo/math/math_test.rs @@ -0,0 +1,991 @@ +//! Tests for `math.cairo`. + +use std::sync::LazyLock; + +use super::math_test_utils::{is_quad_residue_mod_prime, MAX_DIV, RC_BOUND}; +use crate::error_utils::{ + expect_assert_lt_felt252, expect_assert_not_equal_fail, expect_diff_index_comp, + expect_diff_type_comparison, expect_hint_assert_not_zero, expect_hint_out_of_valid_range, + expect_hint_value_outside_250_bit_range, expect_hint_value_outside_valid_range, + expect_non_le_felt252, expect_ok, expect_split_int_limb_out_of_range, + expect_split_int_not_zero, VmCheck, +}; +use cairo_vm::cairo_args; +use cairo_vm::types::builtin_name::BuiltinName; +use cairo_vm::types::program::Program; +use cairo_vm::types::relocatable::MaybeRelocatable; +use cairo_vm::utils::CAIRO_PRIME; +use cairo_vm::vm::runners::cairo_function_runner::CairoFunctionRunner; +use cairo_vm::Felt252; +use num_bigint::{BigInt, BigUint, RandBigInt}; +use num_traits::{One, Signed, Zero}; +use rand::thread_rng; +use rstest::{fixture, rstest}; + +// ===================== Shared constants (LazyLock) ===================== + +/// The compiled Cairo math program, loaded once and shared across all tests. +static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("main_math_test.json")); + +/// Interesting felt values used in several tests. +static INTERESTING_FELTS: LazyLock> = LazyLock::new(|| { + let p = &*CAIRO_PRIME; + vec![ + BigUint::from(0u64), + BigUint::from(1u64), + BigUint::from(2u64).pow(128) - BigUint::one(), + BigUint::from(2u64).pow(128), + BigUint::from(2u64).pow(128) + BigUint::one(), + p / BigUint::from(3u64) - BigUint::one(), + p / BigUint::from(3u64), + p / BigUint::from(3u64) + BigUint::one(), + p / BigUint::from(2u64) - BigUint::one(), + p / BigUint::from(2u64), + p / BigUint::from(2u64) + BigUint::one(), + BigUint::from(2u64).pow(251) - BigUint::one(), + BigUint::from(2u64).pow(251), + BigUint::from(2u64).pow(251) + BigUint::one(), + p - BigUint::from(2u64), + p - BigUint::one(), + ] +}); + +// ===================== Helpers ===================== + +// ===================== Fixture ===================== + +/// Creates a fresh CairoFunctionRunner from the shared PROGRAM. +#[fixture] +fn runner() -> CairoFunctionRunner { + CairoFunctionRunner::new(&PROGRAM).unwrap() +} + +// ===================== test_assert_not_zero ===================== + +#[rstest] +// Case: value=7 +// Expected: Success. +#[case(Some(BigUint::from(7u64)), expect_ok)] +// Case: value=random +// Expected: Success. +#[case::random(None, expect_ok)] +// Case: value=0 +// Expected: Error. +#[case(Some(BigUint::zero()), expect_hint_assert_not_zero)] +fn test_assert_not_zero(#[case] value: Option, #[case] check: VmCheck<()>) { + let value = match value { + Some(v) => v, + None => { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), &CAIRO_PRIME) + } + }; + + let mut runner = runner(); + let args = cairo_args!(value); + let res = runner.run_default_cairo0("assert_not_zero", &args); + check(&res); +} + +// ===================== test_assert_not_equal ===================== + +#[rstest] +// Not equal integers +// Case: a=3, b=7 +// Expected: Success. +#[case::not_equal_ints(MaybeRelocatable::from(3), MaybeRelocatable::from(7), expect_ok)] +// Not equal relocatables (same segment, different offset) +// Case: a=(2, 5), b=(2, 10) +// Expected: Success. +#[case::not_equal_relocs( + MaybeRelocatable::from((2isize, 5)), + MaybeRelocatable::from((2isize, 10)), + expect_ok +)] +// Equal integers +// Case: a=5, b=5 +// Expected: Error. +#[case::equal_ints( + MaybeRelocatable::from(5), + MaybeRelocatable::from(5), + expect_assert_not_equal_fail +)] +// Equal relocatables +// Case: a=(1, 5), b=(1, 5) +// Expected: Error. +#[case::equal_relocs( + MaybeRelocatable::from((1isize, 5)), + MaybeRelocatable::from((1isize, 5)), + expect_assert_not_equal_fail +)] +// Non-comparable: relocatable vs int +// Case: a=(1, 5), b=0 +// Expected: Error. +#[case::non_comparable_reloc_vs_int( + MaybeRelocatable::from((1isize, 5)), + MaybeRelocatable::from(0), + expect_diff_type_comparison +)] +// Non-comparable: different segments +// Case: a=(1, 5), b=(2, 3) +// Expected: Error. +#[case::non_comparable_diff_segments( + MaybeRelocatable::from((1isize, 5)), + MaybeRelocatable::from((2isize, 3)), + expect_diff_index_comp +)] + +fn test_assert_not_equal( + #[case] a: MaybeRelocatable, + #[case] b: MaybeRelocatable, + #[case] check: VmCheck<()>, +) { + let mut runner = runner(); + let args = cairo_args!(a, b); + let res = runner.run_default_cairo0("assert_not_equal", &args); + check(&res); +} + +// ===================== test_assert_250_bit ===================== +#[rstest] +// Valid cases (should pass) +// Case: value=0 +// Expected: Success. +#[case::zero(BigUint::from(0u64), expect_ok)] +// Case: value=1 +// Expected: Success. +#[case::one(BigUint::from(1u64), expect_ok)] +// Case: value=(2^250)-1 +// Expected: Success. +#[case::max_valid(BigUint::from(2u64).pow(250) - BigUint::one(), expect_ok)] +// Invalid cases (should fail) +// Case: value=2^250 +// Expected: Error. +#[case::at_boundary(BigUint::from(2u64).pow(250), expect_hint_value_outside_250_bit_range)] +// Case: value=(2^250)+1 +// Expected: Error. +#[case::above_boundary(BigUint::from(2u64).pow(250) + BigUint::one(), expect_hint_value_outside_250_bit_range)] +// Case: value=2^251 +// Expected: Error. +#[case::way_above(BigUint::from(2u64).pow(251), expect_hint_value_outside_250_bit_range)] +// Case: value=PRIME-1 +// Expected: Error. +#[case::near_prime(&*CAIRO_PRIME - BigUint::one(), expect_hint_value_outside_250_bit_range)] +fn test_assert_250_bit( + mut runner: CairoFunctionRunner, + #[case] value: BigUint, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value); + let res = runner.run_default_cairo0("assert_250_bit", &args); + check(&res); + + // If successful, verify the return value + if res.is_ok() { + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!(&ret[0], &rc_base.add_usize(3usize).unwrap()); + } +} + +// ===================== test_split_felt ===================== + +#[rstest] +// Case: idx=0 +// Expected: Success. +#[case::idx_0(0)] +// Case: idx=1 +// Expected: Success. +#[case::idx_1(1)] +// Case: idx=2 +// Expected: Success. +#[case::idx_2(2)] +// Case: idx=3 +// Expected: Success. +#[case::idx_3(3)] +// Case: idx=4 +// Expected: Success. +#[case::idx_4(4)] +// Case: idx=5 +// Expected: Success. +#[case::idx_5(5)] +// Case: idx=6 +// Expected: Success. +#[case::idx_6(6)] +// Case: idx=7 +// Expected: Success. +#[case::idx_7(7)] +// Case: idx=8 +// Expected: Success. +#[case::idx_8(8)] +// Case: idx=9 +// Expected: Success. +#[case::idx_9(9)] +// Case: idx=10 +// Expected: Success. +#[case::idx_10(10)] +// Case: idx=11 +// Expected: Success. +#[case::idx_11(11)] +// Case: idx=12 +// Expected: Success. +#[case::idx_12(12)] +// Case: idx=13 +// Expected: Success. +#[case::idx_13(13)] +// Case: idx=14 +// Expected: Success. +#[case::idx_14(14)] +// Case: idx=15 +// Expected: Success. +#[case::idx_15(15)] +fn test_split_felt(mut runner: CairoFunctionRunner, #[case] idx: usize) { + let mask_128 = BigUint::from(2u64).pow(128) - BigUint::one(); + let value = &INTERESTING_FELTS[idx]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let expected_high: BigUint = value >> 128; + let expected_low = value & &mask_128; + + let args = cairo_args!(rc_base.clone(), value); + runner + .run_default_cairo0("split_felt", &args) + .unwrap_or_else(|e| panic!("split_felt failed for value {value}: {e}")); + + let ret = runner.get_return_values(3).unwrap(); + // ret = [range_check_ptr, high, low] + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(3usize).unwrap(), + "range_check_ptr mismatch for value {value}" + ); + assert_mr_eq!(&ret[1], &expected_high, "high mismatch for value {value}"); + assert_mr_eq!(&ret[2], &expected_low, "low mismatch for value {value}"); +} + +// ===================== test_assert_le_felt ===================== + +#[rstest] +fn test_assert_le_felt( + mut runner: CairoFunctionRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value0, value1); + + if value0 <= value1 { + runner + .run_default_cairo0("assert_le_felt", &args) + .unwrap_or_else(|e| panic!("assert_le_felt failed for {value0} <= {value1}: {e}")); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(4usize).unwrap(), + "range_check_ptr mismatch for {value0} <= {value1}" + ); + } else { + let result = runner.run_default_cairo0("assert_le_felt", &args); + expect_non_le_felt252(&result); + } +} + +// ===================== test_assert_lt_felt ===================== + +#[rstest] +fn test_assert_lt_felt( + mut runner: CairoFunctionRunner, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx0: usize, + #[values(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)] idx1: usize, +) { + let value0 = &INTERESTING_FELTS[idx0]; + let value1 = &INTERESTING_FELTS[idx1]; + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value0, value1); + + if value0 < value1 { + runner + .run_default_cairo0("assert_lt_felt", &args) + .unwrap_or_else(|e| panic!("assert_lt_felt failed for {value0} < {value1}: {e}")); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(4usize).unwrap(), + "range_check_ptr mismatch for {value0} < {value1}" + ); + } else { + let result = runner.run_default_cairo0("assert_lt_felt", &args); + expect_assert_lt_felt252(&result); + } +} + +// ===================== test_abs_value ===================== + +#[rstest] +// Case: value_case=17 +// Expected: Success. +#[case(BigInt::from(17), expect_ok)] +// Case: value_case=-42 +// Expected: Success. +#[case(BigInt::from(-42), expect_ok)] +// Case: value_case=0 +// Expected: Success. +#[case(BigInt::from(0), expect_ok)] +// Case: value_case=RC_BOUND +// Expected: Error. +#[case(BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +// Case: value_case=-RC_BOUND +// Expected: Error. +#[case(-BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +fn test_abs_value( + mut runner: CairoFunctionRunner, + #[case] value_case: BigInt, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + let rc_bound_biguint = runner + .runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + + let args = cairo_args!(rc_base.clone(), value_case.clone()); + let result = runner.run_default_cairo0("abs_value", &args); + check(&result); + let abs_value = value_case.magnitude(); + if abs_value < &rc_bound_biguint { + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!(&ret[0], &rc_base.add_usize(1usize).unwrap()); + assert_mr_eq!(&ret[1], abs_value); + } +} + +// ===================== test_sign ===================== +#[rstest] +// Case: value_case=17 +// Expected: Success. +#[case(BigInt::from(17), expect_ok)] +// Case: value_case=-42 +// Expected: Success. +#[case(BigInt::from(-42), expect_ok)] +// Case: value_case=0 +// Expected: Success. +#[case(BigInt::from(0), expect_ok)] +// Case: value_case=RC_BOUND +// Expected: Error. +#[case(BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +// Case: value_case=-RC_BOUND +// Expected: Error. +#[case(-BigInt::from(RC_BOUND.clone()), expect_hint_value_outside_valid_range)] +fn test_sign( + mut runner: CairoFunctionRunner, + #[case] value_case: BigInt, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + let rc_bound_biguint = runner + .runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + + let args = cairo_args!(rc_base.clone(), value_case.clone()); + let result = runner.run_default_cairo0("sign", &args); + check(&result); + let abs_value = value_case.magnitude(); + if abs_value < &rc_bound_biguint { + let ret = runner.get_return_values(2).unwrap(); + // range_check_ptr == rc_base + (1 if value != 0 else 0) + let expected_rc_ptr = if value_case.is_zero() { + rc_base + } else { + rc_base.add_usize(1usize).unwrap() + }; + assert_mr_eq!(&ret[0], &expected_rc_ptr); + + // res == (0 if value == 0 else 1 if value > 0 else PRIME - 1) + let expected_sign = if value_case.is_zero() { + BigUint::zero() + } else if value_case.is_positive() { + BigUint::one() + } else { + &*CAIRO_PRIME - BigUint::one() + }; + assert_mr_eq!(&ret[1], &expected_sign); + } +} + +// ===================== test_unsigned_div_rem ===================== + +#[rstest] +// 1) q=1333, div=17, r=3 +// Case: q=1333, div=17, r=3 +// Expected: Success. +#[case::case_1_basic( + Some(BigUint::from(1333u64)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + expect_ok +)] +// 2) q=RC_BOUND-1, div=MAX_DIV, r=MAX_DIV-1 +// Case: q=RC_BOUND-1, div=MAX_DIV, r=MAX_DIV-1 +// Expected: Success. +#[case::case_2_max_values( + Some(&*RC_BOUND - BigUint::one()), + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + expect_ok +)] +// 3) q=random, div=MAX_DIV, r=0 +// Case: q=random, div=MAX_DIV, r=0 +// Expected: Success. +#[case::case_3_random_q( + None, + Some(MAX_DIV.clone()), + Some(BigUint::zero()), + expect_ok +)] +// 4) q=random, div=MAX_DIV, r=MAX_DIV-1 +// Case: q=random, div=MAX_DIV, r=MAX_DIV-1 +// Expected: Success. +#[case::case_4_random_q( + None, + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + expect_ok +)] +// 5) q=random, div=MAX_DIV, r=random +// Case: q=random, div=MAX_DIV, r=random +// Expected: Success. +#[case::case_5_random_q_and_r( + None, + Some(MAX_DIV.clone()), + None, + expect_ok +)] +// 6) q=random, div=random, r=random +// Case: q=random, div=random, r=random +// Expected: Success. +#[case::case_6_all_random(None, None, None, expect_ok)] +// 7) q=1, div=MAX_DIV+1, r=random -> expected error. +// Case: q=1, div=MAX_DIV+1, r=random +// Expected: Error. +#[case::case_7_invalid_div( + Some(BigUint::one()), + Some(&*MAX_DIV + BigUint::one()), + None, + expect_hint_out_of_valid_range +)] +fn test_unsigned_div_rem( + mut runner: CairoFunctionRunner, + #[case] q: Option, + #[case] div: Option, + #[case] r: Option, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + // Verify rc_bound matches expected RC_BOUND (2^128) + let rc_bound = runner + .runner + .vm + .get_range_check_builtin() + .expect("range_check builtin not found") + .bound() + .to_biguint(); + assert_eq!(rc_bound, *RC_BOUND, "Unexpected rc_bound"); + + let mut rng = thread_rng(); + // Python uses div in [0, MAX_DIV], but remainder generation requires div > 0. + let div = match div { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::one(), &(&*MAX_DIV + BigUint::one())), + }; + let r = match r { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &div), + }; + let q = match q { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &RC_BOUND), + }; + + let value = &q * &div + &r; + + // Assert value < PRIME (as in Python test) + assert!( + value < *CAIRO_PRIME, + "Generated value is too large. q={q}, div={div}, r={r}" + ); + + let args = cairo_args!(rc_base.clone(), value, div); + let result = runner.run_default_cairo0("unsigned_div_rem", &args); + check(&result); + + // If successful, verify the results match expected values + if result.is_ok() { + let ret = runner.get_return_values(3).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(3usize).unwrap(), + "range_check_ptr mismatch" + ); + assert_mr_eq!(&ret[1], &q, "quotient mismatch"); + assert_mr_eq!(&ret[2], &r, "remainder mismatch"); + } +} + +// ===================== test_signed_div_rem ===================== +#[rstest] +// Case: q=1333, div=17, r=3, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Success. +#[case::basic( + Some(BigInt::from(1333)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + None, + expect_ok +)] +// Case: q=-1333, div=17, r=3, bound=random in chosen range [-q,RC_BOUND/2]) +// Expected: Success. +#[case::negative_basic( + Some(BigInt::from(-1333)), + Some(BigUint::from(17u64)), + Some(BigUint::from(3u64)), + None, + expect_ok +)] +// Case: q=RC_BOUND/2-1, div=MAX_DIV, r=MAX_DIV-1, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Success. +#[case::max_pos( + Some(BigInt::from(&*RC_BOUND / BigUint::from(2u64) - BigUint::one())), + Some(MAX_DIV.clone()), + Some(&*MAX_DIV - BigUint::one()), + None, + expect_ok +)] +// Case: q=-(RC_BOUND/2)+1, div=MAX_DIV, r=0, bound=random in chosen range [-q,RC_BOUND/2]) +// Expected: Success. +#[case::max_neg( + Some(-(BigInt::from(&*RC_BOUND / BigUint::from(2u64))) + BigInt::one()), + Some(MAX_DIV.clone()), + Some(BigUint::zero()), + None, + expect_ok +)] +// Case: q=random, div=MAX_DIV, r=0, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_r_zero(None, Some(MAX_DIV.clone()), Some(BigUint::zero()), None, expect_ok)] +// Case: q=random, div=MAX_DIV, r=MAX_DIV-1, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_r_max(None, Some(MAX_DIV.clone()), Some(&*MAX_DIV - BigUint::one()), None, expect_ok)] +// Case: q=random, div=MAX_DIV, r=random, bound=random in chosen range ([q+1,RC_BOUND/2] or +// [-q,RC_BOUND/2]) Expected: Success. +#[case::random_q_max_div_random_r(None, Some(MAX_DIV.clone()), None, None, expect_ok)] +// Case: q=RC_BOUND/2-1, div=random, r=random, bound=RC_BOUND/2 +// Expected: Success. +#[case::bound_eq_half_pos_q( + Some(BigInt::from(&*RC_BOUND / BigUint::from(2u64) - BigUint::one())), + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64)), + expect_ok +)] +// Case: q=-RC_BOUND/2, div=random, r=random, bound=RC_BOUND/2 +// Expected: Success. +#[case::bound_eq_half_neg_q( + Some(-BigInt::from(&*RC_BOUND / BigUint::from(2u64))), + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64)), + expect_ok +)] +// Case: q=1, div=MAX_DIV+1, r=random, bound=random in chosen range [q+1,RC_BOUND/2]) +// Expected: Error. +#[case::invalid_div( + Some(BigInt::one()), + Some(&*MAX_DIV + BigUint::one()), + None, + None, + expect_hint_out_of_valid_range +)] +// Case: q=random, div=random, r=random, bound=RC_BOUND/2+1 +// Expected: Error. +#[case::invalid_bound( + None, + None, + None, + Some(&*RC_BOUND / BigUint::from(2u64) + BigUint::one()), + expect_hint_out_of_valid_range +)] +fn test_signed_div_rem( + mut runner: CairoFunctionRunner, + #[case] q: Option, + #[case] div: Option, + #[case] r: Option, + #[case] bound: Option, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let half_rc_bound = &*RC_BOUND / BigUint::from(2u64); + let mut rng = thread_rng(); + + let div = match div { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::one(), &(&*MAX_DIV + BigUint::one())), + }; + let r = match r { + Some(v) => v, + None => rng.gen_biguint_range(&BigUint::zero(), &div), + }; + let q = match q { + Some(v) => v, + None => { + let min = -BigInt::from(half_rc_bound.clone()); + let max = BigInt::from(half_rc_bound.clone()); + rng.gen_bigint_range(&min, &max) + } + }; + let bound = match bound { + Some(v) => v, + None => { + let lower = if q >= BigInt::zero() { + q.clone() + BigInt::one() + } else { + -q.clone() + }; + let upper = BigInt::from(half_rc_bound.clone() + BigUint::one()); + rng.gen_bigint_range(&lower, &upper) + .to_biguint() + .expect("bound should be non-negative") + } + }; + + let value = q.clone() * BigInt::from(div.clone()) + BigInt::from(r.clone()); + let half_prime = BigInt::from((&*CAIRO_PRIME) >> 1usize); + let neg_half_prime = -half_prime.clone(); + assert!( + value >= neg_half_prime && value < half_prime, + "Generated value is too large." + ); + + let args = cairo_args!(rc_base.clone(), value, div, bound); + let result = runner.run_default_cairo0("signed_div_rem", &args); + check(&result); + + if result.is_ok() { + let ret = runner.get_return_values(3).unwrap(); + let rc_ptr = &ret[0]; + let result_q = &ret[1]; + let result_r = &ret[2]; + + assert_mr_eq!(rc_ptr, &rc_base.add_usize(4usize).unwrap()); + // Expected_q = q % PRIME (field element conversion). + let expected_q = Felt252::from(&q); + assert_mr_eq!(result_q, &expected_q); + assert_mr_eq!(result_r, &r); + } +} + +// ===================== test_split_int ===================== +#[rstest] +// Case: value=0x1234FCDA, n=10, base=16, bound=16, expected_output=vec![0xA, 0xD, +// 0xC, 0xF, 0x4, 0x3, 0x2, 0x1, 0, 0] Expected: Success. +#[case::hex_digits( + 0x1234FCDA_i64, + 10_i64, + 16_i64, + 16_i64, + Some(vec![0xA, 0xD, 0xC, 0xF, 0x4, 0x3, 0x2, 0x1, 0, 0]), + expect_ok +)] +// Case: value=0x1234FCDA, n=10, base=256, bound=256, expected_output=vec![0xDA, +// 0xFC, 0x34, 0x12, 0, 0, 0, 0, 0, 0] Expected: Success. +#[case::byte_pairs( + 0x1234FCDA_i64, + 10_i64, + 256_i64, + 256_i64, + Some(vec![0xDA, 0xFC, 0x34, 0x12, 0, 0, 0, 0, 0, 0]), + expect_ok +)] +// Case: value=0x1234FCDA, n=10, base=16, bound=15, expected_output=random +// Expected: Error. +#[case::out_of_bound_limb( + 0x1234FCDA_i64, + 10_i64, + 16_i64, + 15_i64, + None, + expect_split_int_limb_out_of_range +)] +// Case: value=0xAAA, n=3, base=16, bound=11, expected_output=vec![0xA, 0xA, 0xA] +// Expected: Success. +#[case::exact_fit( + 0xAAA_i64, + 3_i64, + 16_i64, + 11_i64, + Some(vec![0xA, 0xA, 0xA]), + expect_ok +)] +// Case: value=0xAAA, n=3, base=16, bound=10, expected_output=random +// Expected: Error. +#[case::bound_too_small( + 0xAAA_i64, + 3_i64, + 16_i64, + 10_i64, + None, + expect_split_int_limb_out_of_range +)] +// Case: value=0xAAA, n=2, base=16, bound=16, expected_output=random +// Expected: Error. +#[case::value_out_of_range(0xAAA_i64, 2_i64, 16_i64, 16_i64, None, expect_split_int_not_zero)] +fn test_split_int( + mut runner: CairoFunctionRunner, + #[case] value: i64, + #[case] n: i64, + #[case] base: i64, + #[case] bound: i64, + #[case] expected_output: Option>, + #[case] check: VmCheck<()>, +) { + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let output = runner.runner.vm.add_memory_segment(); + let output_mr = MaybeRelocatable::from(output); + + let args = cairo_args!(rc_base.clone(), value, n, base, bound, output_mr); + let result = runner.run_default_cairo0("split_int", &args); + check(&result); + + if result.is_ok() { + let expected_output = + expected_output.expect("expected_output must be set for success case"); + let ret = runner.get_return_values(1).unwrap(); + assert_mr_eq!(&ret[0], &rc_base.add_usize(2usize * n as usize).unwrap()); + + let range = runner.runner.vm.get_range(output, n as usize); + assert_eq!( + range.len(), + expected_output.len(), + "split_int output length mismatch" + ); + for (i, (actual, exp)) in range.iter().zip(expected_output.iter()).enumerate() { + let actual_val = actual + .as_ref() + .unwrap_or_else(|| panic!("Missing output at index {i}")); + assert_mr_eq!( + actual_val.as_ref(), + *exp, + "split_int output mismatch at index {i}" + ); + } + } +} +// ===================== test_sqrt ===================== + +#[rstest] +// Case: value=0 +// Expected: Success. +#[case::zero(Some(BigUint::from(0u64)), expect_ok)] +// Case: value=1 +// Expected: Success. +#[case::one(Some(BigUint::from(1u64)), expect_ok)] +// Case: value=2 +// Expected: Success. +#[case::two(Some(BigUint::from(2u64)), expect_ok)] +// Case: value=3 +// Expected: Success. +#[case::three(Some(BigUint::from(3u64)), expect_ok)] +// Case: value=4 +// Expected: Success. +#[case::four(Some(BigUint::from(4u64)), expect_ok)] +// Case: value=5 +// Expected: Success. +#[case::five(Some(BigUint::from(5u64)), expect_ok)] +// Case: value=6 +// Expected: Success. +#[case::six(Some(BigUint::from(6u64)), expect_ok)] +// Case: value=7 +// Expected: Success. +#[case::seven(Some(BigUint::from(7u64)), expect_ok)] +// Case: value=8 +// Expected: Success. +#[case::eight(Some(BigUint::from(8u64)), expect_ok)] +// Case: value=9 +// Expected: Success. +#[case::nine(Some(BigUint::from(9u64)), expect_ok)] +// Case: value=(2^250)-1 +// Expected: Success. +#[case::max_valid(Some(BigUint::from(2u64).pow(250) - BigUint::one()), expect_ok)] +// Case: value=random +// Expected: Success. +#[case::random(None, expect_ok)] +// Case: value=2^250 +// Expected: Error. +#[case::out_of_range_2_pow_250(Some(BigUint::from(2u64).pow(250)), expect_hint_value_outside_250_bit_range)] +// Case: value=PRIME-1 +// Expected: Error. +#[case::out_of_range_prime_minus_one( + Some(&*CAIRO_PRIME - BigUint::one()), + expect_hint_value_outside_250_bit_range +)] +fn test_sqrt( + mut runner: CairoFunctionRunner, + #[case] value: Option, + #[case] check: VmCheck<()>, +) { + let value = value.unwrap_or_else(|| { + let mut rng = thread_rng(); + let upper = BigUint::one() << 250usize; + rng.gen_biguint_range(&BigUint::zero(), &upper) + }); + + let rc_base = runner + .get_builtin_base(BuiltinName::range_check) + .expect("range_check builtin not found"); + + let args = cairo_args!(rc_base.clone(), value.clone()); + let result = runner.run_default_cairo0("sqrt", &args); + check(&result); + + if result.is_ok() { + let ret = runner.get_return_values(2).unwrap(); + assert_mr_eq!( + &ret[0], + &rc_base.add_usize(4usize).unwrap(), + "range_check_ptr mismatch for sqrt({value})" + ); + + let expected_root = value.sqrt(); + assert_mr_eq!( + &ret[1], + &expected_root, + "sqrt result mismatch for value={value}" + ); + } +} + +// ===================== test_horner_eval ===================== + +#[rstest] +// Case: n=0 +// Expected: Success. +#[case::zero_coefficients(0)] +// Case: n=16 +// Expected: Success. +#[case::sixteen_coefficients(16)] +fn test_horner_eval(mut runner: CairoFunctionRunner, #[case] n: usize) { + let mut rng = thread_rng(); + let prime = &*CAIRO_PRIME; + + // Generate random coefficients in [0, PRIME) + let coefficients: Vec = (0..n) + .map(|_| rng.gen_biguint_range(&BigUint::zero(), prime)) + .collect(); + let coeff_mr: Vec = coefficients.iter().map(MaybeRelocatable::from).collect(); + + // Generate random point in [0, PRIME) + let point = rng.gen_biguint_range(&BigUint::zero(), prime); + + // horner_eval takes (n, coefficients_ptr, point) - coefficients is an array + let args = cairo_args!(n, coeff_mr, point.clone()); + runner.run_default_cairo0("horner_eval", &args).unwrap(); + + let ret = runner.get_return_values(1).unwrap(); + + // Compute expected result: sum(coef * point^i for i, coef in enumerate(coefficients)) % PRIME + let expected: BigUint = coefficients + .iter() + .enumerate() + .map(|(i, coef)| coef * point.modpow(&BigUint::from(i), prime)) + .fold(BigUint::zero(), |acc, x| (acc + x) % prime); + + assert_mr_eq!(&ret[0], &expected); +} + +// ===================== test_is_quad_residue ===================== + +#[rstest] +// Case: x=0 +// Expected: Success. +#[case::zero(Some(BigUint::zero()))] +// Case: x=random +// Expected: Success. +#[case::random(None)] +fn test_is_quad_residue(mut runner: CairoFunctionRunner, #[case] x: Option) { + let prime = &*CAIRO_PRIME; + + let x = x.unwrap_or_else(|| { + let mut rng = thread_rng(); + rng.gen_biguint_range(&BigUint::one(), prime) + }); + + // Test is_quad_residue(x) + let args = cairo_args!(x.clone()); + runner.run_default_cairo0("is_quad_residue", &args).unwrap(); + let ret = runner.get_return_values(1).unwrap(); + + let expected = is_quad_residue_mod_prime(&x); + assert_mr_eq!( + &ret[0], + expected, + "is_quad_residue({x}) should return {expected}" + ); + + // Test is_quad_residue(3 * x) + // 3 is not a quadratic residue modulo PRIME + let mut runner2 = CairoFunctionRunner::new(&PROGRAM).unwrap(); + let three_x = (BigUint::from(3u64) * &x) % prime; + let args2 = cairo_args!(three_x); + runner2 + .run_default_cairo0("is_quad_residue", &args2) + .unwrap(); + let ret2 = runner2.get_return_values(1).unwrap(); + + let expected2 = if x.is_zero() { + 1i64 // 3 * 0 = 0, which is QR + } else if is_quad_residue_mod_prime(&x) == 1 { + 0i64 // x is QR, 3 is not QR, so 3*x is not QR + } else { + 1i64 // x is not QR, 3 is not QR, so 3*x is QR (product of two non-QR is QR) + }; + assert_mr_eq!( + &ret2[0], + expected2, + "is_quad_residue(3 * {x}) should return {expected2}" + ); +} diff --git a/tests_cairo/math/math_test_utils.rs b/tests_cairo/math/math_test_utils.rs new file mode 100644 index 0000000000..31f267e6fb --- /dev/null +++ b/tests_cairo/math/math_test_utils.rs @@ -0,0 +1,27 @@ +use std::sync::LazyLock; + +use cairo_vm::{math_utils::is_quad_residue, utils::CAIRO_PRIME}; +use num_bigint::BigUint; +use num_integer::Integer; +/// RC_BOUND = 2^128 +pub static RC_BOUND: LazyLock = LazyLock::new(|| BigUint::from(2u64).pow(128)); + +/// MAX_DIV = CAIRO_PRIME // RC_BOUND +pub static MAX_DIV: LazyLock = LazyLock::new(|| CAIRO_PRIME.div_floor(&RC_BOUND)); + +/// Returns 1 if `a` is a quadratic residue modulo CAIRO_PRIME, 0 if not, and -1 on error. +pub fn is_quad_residue_mod_prime(a: &BigUint) -> i64 { + match is_quad_residue(a, &CAIRO_PRIME) { + Ok(true) => 1, + Ok(false) => 0, + Err(_) => -1, + } +} +/// Computes `(b - a) mod CAIRO_PRIME`. +pub fn sub_mod_prime(a: &BigUint, b: &BigUint) -> BigUint { + if b >= a { + (b - a) % &*CAIRO_PRIME + } else { + (&*CAIRO_PRIME + b - a) % &*CAIRO_PRIME + } +} diff --git a/tests_cairo/math/mod.rs b/tests_cairo/math/mod.rs new file mode 100644 index 0000000000..bda0d70e4f --- /dev/null +++ b/tests_cairo/math/mod.rs @@ -0,0 +1,3 @@ +mod math_cmp_test; +mod math_test; +mod math_test_utils; diff --git a/tests_cairo/mod.rs b/tests_cairo/mod.rs new file mode 100644 index 0000000000..8db59cc439 --- /dev/null +++ b/tests_cairo/mod.rs @@ -0,0 +1,6 @@ +#![cfg(test)] + +#[macro_use] +mod test_utils; +mod error_utils; +mod math; diff --git a/tests_cairo/test_utils.rs b/tests_cairo/test_utils.rs new file mode 100644 index 0000000000..e8a529ac66 --- /dev/null +++ b/tests_cairo/test_utils.rs @@ -0,0 +1,56 @@ +/// Loads a compiled Cairo `.json` program from the same directory as the calling source file. +/// +/// Pass only the filename (no directory prefix). The directory is inferred from the call site +/// via `file!()`, so the `.json` must live next to the `.cairo` and `.rs` files. +/// +/// # Example +/// ```rust +/// static PROGRAM: LazyLock = LazyLock::new(|| load_cairo_program!("main_math_test.json")); +/// ``` +/// +/// # Panics +/// - If the `.json` file does not exist: run `make tests_cairo_programs` first. +/// - If the `.json` file cannot be parsed as a Cairo `Program`. +#[macro_export] +macro_rules! load_cairo_program { + ($name:literal) => {{ + // file!() expands at the call site, giving the path of the calling source file + // relative to the workspace root (e.g. "tests_cairo/math/math_test.rs"). + // We derive the directory from it and join with the requested filename. + let source_dir = std::path::Path::new(file!()) + .parent() + .expect("source file should have a parent directory"); + // CARGO_MANIFEST_DIR is the `vm/` crate dir; workspace root is one level up. + let workspace_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("vm crate should have a parent directory"); + let json_path = workspace_root.join(source_dir).join($name); + + let bytes = std::fs::read(&json_path).unwrap_or_else(|err| { + panic!( + "Cairo program not found at {json_path:?}: {err}\n\ + Did you run `make tests_cairo_programs`?" + ) + }); + + cairo_vm::types::program::Program::from_bytes(&bytes, None) + .unwrap_or_else(|e| panic!("Failed to parse Cairo program at {json_path:?}: {e}")) + }}; +} + +/// Asserts that a `MaybeRelocatable` reference equals a value convertible into `MaybeRelocatable`. +#[macro_export] +macro_rules! assert_mr_eq { + ($left:expr, $right:expr) => {{ + let right_mr = ($right) + .try_into() + .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + assert_eq!($left, &right_mr); + }}; + ($left:expr, $right:expr, $($arg:tt)+) => {{ + let right_mr = ($right) + .try_into() + .unwrap_or_else(|e| panic!("conversion to MaybeRelocatable failed: {e:?}")); + assert_eq!($left, &right_mr, $($arg)+); + }}; +} diff --git a/vm/Cargo.toml b/vm/Cargo.toml index 03ec972fb9..2db9687648 100644 --- a/vm/Cargo.toml +++ b/vm/Cargo.toml @@ -21,7 +21,11 @@ cairo-0-secp-hints = [] cairo-0-data-availability-hints = [] # Note that these features are not retro-compatible with the cairo Python VM. -test_utils = ["dep:arbitrary", "starknet-types-core/arbitrary", "starknet-types-core/std"] # This feature will reference every test-oriented feature +test_utils = [ + "dep:arbitrary", + "starknet-types-core/arbitrary", + "starknet-types-core/std", +] # This feature will reference every test-oriented feature # Allows extending the set of hints for the current vm run from within a hint. # For a usage example checkout vm/src/tests/run_deprecated_contract_class_simplified.rs extensive_hints = [] @@ -45,7 +49,12 @@ generic-array = { workspace = true } keccak = { workspace = true } anyhow = { workspace = true } thiserror = { workspace = true } -starknet-types-core = { version = "0.2.4", default-features = false, features = ["serde", "curve", "num-traits", "hash"] } +starknet-types-core = { version = "0.2.4", default-features = false, features = [ + "serde", + "curve", + "num-traits", + "hash", +] } rust_decimal = { version = "1.35.0", default-features = false } num-prime = { version = "0.4.3", features = ["big-int"] } @@ -61,7 +70,7 @@ ark-std = { workspace = true, optional = true } arbitrary = { workspace = true, features = ["derive"], optional = true } # Used to derive clap traits for CLIs -clap = { version = "4.3.10", features = ["derive"], optional = true} +clap = { version = "4.3.10", features = ["derive"], optional = true } [dev-dependencies] assert_matches = "1.5.0" @@ -81,6 +90,10 @@ path = "../bench/criterion_benchmark.rs" name = "criterion_benchmark" harness = false +[[test]] +name = "math_test" +path = "../tests_cairo/mod.rs" + [[example]] name = "custom_hint" path = "../examples/custom_hint/src/main.rs" diff --git a/vm/src/cairo_run.rs b/vm/src/cairo_run.rs index 6203ffa66d..3f206d5d2c 100644 --- a/vm/src/cairo_run.rs +++ b/vm/src/cairo_run.rs @@ -8,7 +8,10 @@ use crate::{ errors::{ cairo_run_errors::CairoRunError, runner_errors::RunnerError, vm_exception::VmException, }, - runners::{cairo_pie::CairoPie, cairo_runner::CairoRunner}, + runners::{ + cairo_pie::CairoPie, + cairo_runner::{CairoRunner, RunnerMode}, + }, security::verify_secure_runner, trace::trace_entry::RelocatedTraceEntry, }, @@ -67,6 +70,82 @@ impl Default for CairoRunConfig<'_> { } } +pub struct StwoCairoRunConfig { + pub trace_enabled: bool, + pub relocate_mem: bool, + pub relocate_trace: bool, + pub fill_holes: bool, + pub secure_run: bool, + pub disable_trace_padding: bool, +} + +impl Default for StwoCairoRunConfig { + fn default() -> Self { + StwoCairoRunConfig { + trace_enabled: true, + relocate_mem: false, + relocate_trace: true, + fill_holes: false, + secure_run: true, + disable_trace_padding: true, + } + } +} + +#[allow(clippy::result_large_err)] +pub fn cairo_run_stwo( + program: &Program, + runner_mode: RunnerMode, + allowed_builtins: &[BuiltinName], + hint_processor: &mut dyn HintProcessor, + exec_scopes: ExecutionScopes, + cairo_run_config: &StwoCairoRunConfig, +) -> Result { + let _span = span!(Level::INFO, "cairo run stwo").entered(); + + let proof_mode = runner_mode != RunnerMode::ExecutionMode; + let mut cairo_runner = CairoRunner::new_stwo( + program, + runner_mode, + cairo_run_config.trace_enabled, + cairo_run_config.disable_trace_padding, + )?; + cairo_runner.exec_scopes = exec_scopes; + + let end = cairo_runner.initialize_stwo(allowed_builtins)?; + + cairo_runner + .run_until_pc(end, hint_processor) + .map_err(|err| VmException::from_vm_error(&cairo_runner, err))?; + + if proof_mode { + cairo_runner.run_for_steps(1, hint_processor)?; + } + + cairo_runner.end_run( + cairo_run_config.disable_trace_padding, + false, + hint_processor, + cairo_run_config.fill_holes, + )?; + + cairo_runner.read_return_values(false)?; + if proof_mode { + cairo_runner.finalize_segments()?; + } + + if cairo_run_config.secure_run { + verify_secure_runner(&cairo_runner, true, None)?; + } + + cairo_runner.relocate( + cairo_run_config.relocate_mem, + cairo_run_config.relocate_trace, + )?; + + Ok(cairo_runner) +} + #[allow(clippy::result_large_err)] /// Runs a program with a customized execution scope. pub fn cairo_run_program_with_initial_scope( @@ -251,6 +330,86 @@ pub fn cairo_run_pie( Ok(cairo_runner) } +/// Runs a Cairo PIE using the Stwo runtime API. +/// Same as `cairo_run_pie` but uses `new_stwo` + `initialize_stwo` instead of layouts. +/// Note: Cairo PIEs cannot be run in proof mode. +/// WARNING: As the RunResources are part of the HintProcessor trait, the caller should make sure that +/// the number of steps in the `RunResources` matches that of the `ExecutionResources` in the `CairoPie`. +/// An error will be returned if this doesn't hold. +#[allow(clippy::result_large_err)] +pub fn cairo_run_pie_stwo( + pie: &CairoPie, + allowed_builtins: &[BuiltinName], + hint_processor: &mut dyn HintProcessor, + cairo_run_config: &StwoCairoRunConfig, +) -> Result { + if hint_processor + .get_n_steps() + .is_none_or(|steps| steps != pie.execution_resources.n_steps) + { + return Err(RunnerError::PieNStepsVsRunResourcesNStepsMismatch.into()); + } + pie.run_validity_checks()?; + + let program = Program::from_stripped_program(&pie.metadata.program); + let mut cairo_runner = CairoRunner::new_stwo( + &program, + RunnerMode::ExecutionMode, + cairo_run_config.trace_enabled, + cairo_run_config.disable_trace_padding, + )?; + + let end = cairo_runner.initialize_stwo(allowed_builtins)?; + cairo_runner.vm.finalize_segments_by_cairo_pie(pie); + // Load builtin additional data + for (name, data) in pie.additional_data.0.iter() { + // Data is not trusted in secure_run, therefore we skip extending the hash builtin's data + if matches!(name, BuiltinName::pedersen) && cairo_run_config.secure_run { + continue; + } + if let Some(builtin) = cairo_runner + .vm + .builtin_runners + .iter_mut() + .find(|b| b.name() == *name) + { + builtin.extend_additional_data(data)?; + } + } + // Load previous execution memory + let has_zero_segment = cairo_runner.vm.segments.has_zero_segment() as usize; + let n_extra_segments = pie.metadata.extra_segments.len() - has_zero_segment; + cairo_runner + .vm + .segments + .load_pie_memory(&pie.memory, n_extra_segments)?; + + cairo_runner + .run_until_pc(end, hint_processor) + .map_err(|err| VmException::from_vm_error(&cairo_runner, err))?; + + cairo_runner.end_run( + cairo_run_config.disable_trace_padding, + false, + hint_processor, + cairo_run_config.fill_holes, + )?; + + cairo_runner.read_return_values(false)?; + + if cairo_run_config.secure_run { + verify_secure_runner(&cairo_runner, true, None)?; + // Check that the Cairo PIE produced by this run is compatible with the Cairo PIE received + cairo_runner.get_cairo_pie()?.check_pie_compatibility(pie)?; + } + cairo_runner.relocate( + cairo_run_config.relocate_mem, + cairo_run_config.relocate_trace, + )?; + + Ok(cairo_runner) +} + #[cfg(feature = "test_utils")] #[allow(clippy::result_large_err)] pub fn cairo_run_fuzzed_program( @@ -553,6 +712,184 @@ mod tests { ))); } + fn make_cairo_pie(program_content: &[u8]) -> CairoPie { + let runner = cairo_run( + program_content, + &CairoRunConfig { + layout: LayoutName::all_cairo_stwo, + ..Default::default() + }, + &mut BuiltinHintProcessor::new_empty(), + ) + .unwrap(); + runner.get_cairo_pie().unwrap() + } + + fn stwo_allowed_builtins() -> Vec { + let mut allowed = vec![ + BuiltinName::output, + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::bitwise, + BuiltinName::ec_op, + BuiltinName::poseidon, + BuiltinName::range_check96, + ]; + if cfg!(feature = "mod_builtin") { + allowed.push(BuiltinName::add_mod); + allowed.push(BuiltinName::mul_mod); + } + allowed + } + + fn make_cairo_pie_stwo(program_content: &[u8]) -> CairoPie { + let program = Program::from_bytes(program_content, Some("main")).unwrap(); + let runner = cairo_run_stwo( + &program, + RunnerMode::ExecutionMode, + &stwo_allowed_builtins(), + &mut BuiltinHintProcessor::new_empty(), + ExecutionScopes::new(), + &StwoCairoRunConfig { + disable_trace_padding: false, + ..Default::default() + }, + ) + .unwrap(); + runner.get_cairo_pie().unwrap() + } + + fn stwo_pie_config() -> StwoCairoRunConfig { + StwoCairoRunConfig { + disable_trace_padding: false, + ..Default::default() + } + } + + #[rstest] + #[case(include_bytes!("../../cairo_programs/fibonacci.json"))] + #[case(include_bytes!("../../cairo_programs/integration.json"))] + #[case(include_bytes!("../../cairo_programs/relocate_segments.json"))] + #[case(include_bytes!("../../cairo_programs/ec_op.json"))] + #[case(include_bytes!("../../cairo_programs/bitwise_output.json"))] + fn get_and_run_cairo_pie_stwo(#[case] program_content: &[u8]) { + let cairo_pie = make_cairo_pie_stwo(program_content); + let allowed: Vec = cairo_pie + .metadata + .builtin_segments + .keys() + .copied() + .collect(); + let mut hint_processor = BuiltinHintProcessor::new( + Default::default(), + RunResources::new(cairo_pie.execution_resources.n_steps), + ); + let config = stwo_pie_config(); + let runner = + cairo_run_pie_stwo(&cairo_pie, &allowed, &mut hint_processor, &config).unwrap(); + assert!(runner.relocated_trace.is_some()); + } + + #[rstest] + #[case(include_bytes!("../../cairo_programs/fibonacci.json"))] + #[case(include_bytes!("../../cairo_programs/bitwise_output.json"))] + fn cairo_run_pie_stwo_matches_legacy(#[case] program_content: &[u8]) { + let cairo_pie = make_cairo_pie(program_content); + let allowed: Vec = cairo_pie + .metadata + .builtin_segments + .keys() + .copied() + .collect(); + let legacy_runner = cairo_run_pie( + &cairo_pie, + &CairoRunConfig { + layout: LayoutName::all_cairo_stwo, + trace_enabled: true, + relocate_mem: true, + ..Default::default() + }, + &mut BuiltinHintProcessor::new( + Default::default(), + RunResources::new(cairo_pie.execution_resources.n_steps), + ), + ) + .unwrap(); + let stwo_config = StwoCairoRunConfig { + relocate_mem: true, + ..stwo_pie_config() + }; + let stwo_runner = cairo_run_pie_stwo( + &cairo_pie, + &allowed, + &mut BuiltinHintProcessor::new( + Default::default(), + RunResources::new(cairo_pie.execution_resources.n_steps), + ), + &stwo_config, + ) + .unwrap(); + assert_eq!(legacy_runner.relocated_memory, stwo_runner.relocated_memory); + assert_eq!(legacy_runner.relocated_trace, stwo_runner.relocated_trace); + } + + #[test] + fn cairo_run_pie_stwo_n_steps_not_set() { + let cairo_pie = make_cairo_pie(include_bytes!("../../cairo_programs/fibonacci.json")); + let res = cairo_run_pie_stwo( + &cairo_pie, + &[], + &mut BuiltinHintProcessor::new_empty(), + &stwo_pie_config(), + ); + assert!(res.is_err_and(|err| matches!( + err, + CairoRunError::Runner(RunnerError::PieNStepsVsRunResourcesNStepsMismatch) + ))); + } + + #[test] + fn cairo_run_pie_stwo_n_steps_mismatch() { + let cairo_pie = make_cairo_pie(include_bytes!("../../cairo_programs/fibonacci.json")); + let wrong_steps = cairo_pie.execution_resources.n_steps + 1; + let res = cairo_run_pie_stwo( + &cairo_pie, + &[], + &mut BuiltinHintProcessor::new(Default::default(), RunResources::new(wrong_steps)), + &stwo_pie_config(), + ); + assert!(res.is_err_and(|err| matches!( + err, + CairoRunError::Runner(RunnerError::PieNStepsVsRunResourcesNStepsMismatch) + ))); + } + + #[test] + fn cairo_run_pie_stwo_without_secure_run() { + let cairo_pie = make_cairo_pie(include_bytes!("../../cairo_programs/fibonacci.json")); + let allowed: Vec = cairo_pie + .metadata + .builtin_segments + .keys() + .copied() + .collect(); + let config = StwoCairoRunConfig { + secure_run: false, + ..stwo_pie_config() + }; + let runner = cairo_run_pie_stwo( + &cairo_pie, + &allowed, + &mut BuiltinHintProcessor::new( + Default::default(), + RunResources::new(cairo_pie.execution_resources.n_steps), + ), + &config, + ) + .unwrap(); + assert!(runner.relocated_trace.is_some()); + } + /// A simple slice writer for testing BinaryWrite in no_std-like conditions. struct SliceWriter<'a> { buf: &'a mut [u8], diff --git a/vm/src/math_utils/mod.rs b/vm/src/math_utils/mod.rs index aa77df4219..f150f4c900 100644 --- a/vm/src/math_utils/mod.rs +++ b/vm/src/math_utils/mod.rs @@ -397,7 +397,7 @@ fn legendre_symbol(a: &BigUint, p: &BigUint) -> i8 { // Ported from sympy implementation // Simplified as a & p are nonnegative // Asumes p is a prime number -pub(crate) fn is_quad_residue(a: &BigUint, p: &BigUint) -> Result { +pub fn is_quad_residue(a: &BigUint, p: &BigUint) -> Result { if p.is_zero() { return Err(MathError::IsQuadResidueZeroPrime); } diff --git a/vm/src/tests/mod.rs b/vm/src/tests/mod.rs index 2746df2252..331c1d3e06 100644 --- a/vm/src/tests/mod.rs +++ b/vm/src/tests/mod.rs @@ -10,7 +10,10 @@ use crate::Felt252; use crate::{ hint_processor::cairo_1_hint_processor::hint_processor::Cairo1HintProcessor, types::{builtin_name::BuiltinName, relocatable::MaybeRelocatable}, - vm::runners::cairo_runner::{CairoArg, CairoRunner}, + vm::runners::{ + cairo_function_runner::{CairoFunctionRunner, EntryPoint}, + cairo_runner::CairoArg, + }, }; #[cfg(feature = "cairo-1-hints")] use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass; @@ -104,7 +107,7 @@ fn run_cairo_1_entrypoint( let mut hint_processor = Cairo1HintProcessor::new(&contract_class.hints, RunResources::default(), false); - let mut runner = CairoRunner::new( + let mut function_runner = CairoFunctionRunner::new_custom( &(contract_class.clone().try_into().unwrap()), LayoutName::all_cairo, None, @@ -115,16 +118,16 @@ fn run_cairo_1_entrypoint( .unwrap(); let program_builtins = get_casm_contract_builtins(&contract_class, entrypoint_offset); - runner + function_runner .initialize_function_runner_cairo_1(&program_builtins) .unwrap(); // Implicit Args - let syscall_segment = MaybeRelocatable::from(runner.vm.add_memory_segment()); + let syscall_segment = MaybeRelocatable::from(function_runner.vm.add_memory_segment()); - let builtins = runner.get_program_builtins(); + let builtins = function_runner.get_program_builtins(); - let builtin_segment: Vec = runner + let builtin_segment: Vec = function_runner .vm .get_builtin_runners() .iter() @@ -141,27 +144,33 @@ fn run_cairo_1_entrypoint( // Other args // Load builtin costs - let builtin_costs: Vec = - vec![0.into(), 0.into(), 0.into(), 0.into(), 0.into()]; - let builtin_costs_ptr = runner.vm.add_memory_segment(); - runner + let builtin_costs: Vec = vec![ + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + ]; + let builtin_costs_ptr = function_runner.vm.add_memory_segment(); + function_runner .vm .load_data(builtin_costs_ptr, &builtin_costs) .unwrap(); // Load extra data - let core_program_end_ptr = - (runner.program_base.unwrap() + runner.program.shared_program_data.data.len()).unwrap(); + let core_program_end_ptr = (function_runner.program_base.unwrap() + + function_runner.program.shared_program_data.data.len()) + .unwrap(); let program_extra_data: Vec = - vec![0x208B7FFF7FFF7FFE.into(), builtin_costs_ptr.into()]; - runner + vec![0x208B7FFF7FFF7FFE_u64.into(), builtin_costs_ptr.into()]; + function_runner .vm .load_data(core_program_end_ptr, &program_extra_data) .unwrap(); // Load calldata - let calldata_start = runner.vm.add_memory_segment(); - let calldata_end = runner.vm.load_data(calldata_start, args).unwrap(); + let calldata_start = function_runner.vm.add_memory_segment(); + let calldata_end = function_runner.vm.load_data(calldata_start, args).unwrap(); // Create entrypoint_args @@ -173,25 +182,26 @@ fn run_cairo_1_entrypoint( MaybeRelocatable::from(calldata_start).into(), MaybeRelocatable::from(calldata_end).into(), ]); - let entrypoint_args: Vec<&CairoArg> = entrypoint_args.iter().collect(); // Run contract entrypoint - runner - .run_from_entrypoint( - entrypoint_offset, - &entrypoint_args, + let program_segment_size = + function_runner.program.shared_program_data.data.len() + program_extra_data.len(); + function_runner + .run( + EntryPoint::Pc(entrypoint_offset), true, - Some(runner.program.shared_program_data.data.len() + program_extra_data.len()), + Some(program_segment_size), &mut hint_processor, + &entrypoint_args, ) .unwrap(); // Check return values - let return_values = runner.vm.get_return_values(5).unwrap(); + let return_values = function_runner.vm.get_return_values(5).unwrap(); let retdata_start = return_values[3].get_relocatable().unwrap(); let retdata_end = return_values[4].get_relocatable().unwrap(); - let retdata: Vec = runner + let retdata: Vec = function_runner .vm .get_integer_range(retdata_start, (retdata_end - retdata_start).unwrap()) .unwrap() @@ -211,7 +221,7 @@ fn run_cairo_1_entrypoint_with_run_resources( hint_processor: &mut Cairo1HintProcessor, args: &[MaybeRelocatable], ) -> Result, CairoRunError> { - let mut runner = CairoRunner::new( + let mut function_runner = CairoFunctionRunner::new_custom( &(contract_class.clone().try_into().unwrap()), LayoutName::all_cairo, None, @@ -222,16 +232,16 @@ fn run_cairo_1_entrypoint_with_run_resources( .unwrap(); let program_builtins = get_casm_contract_builtins(&contract_class, entrypoint_offset); - runner + function_runner .initialize_function_runner_cairo_1(&program_builtins) .unwrap(); // Implicit Args - let syscall_segment = MaybeRelocatable::from(runner.vm.add_memory_segment()); + let syscall_segment = MaybeRelocatable::from(function_runner.vm.add_memory_segment()); - let builtins = runner.get_program_builtins(); + let builtins = function_runner.get_program_builtins(); - let builtin_segment: Vec = runner + let builtin_segment: Vec = function_runner .vm .get_builtin_runners() .iter() @@ -248,27 +258,33 @@ fn run_cairo_1_entrypoint_with_run_resources( // Other args // Load builtin costs - let builtin_costs: Vec = - vec![0.into(), 0.into(), 0.into(), 0.into(), 0.into()]; - let builtin_costs_ptr = runner.vm.add_memory_segment(); - runner + let builtin_costs: Vec = vec![ + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + 0_i64.into(), + ]; + let builtin_costs_ptr = function_runner.vm.add_memory_segment(); + function_runner .vm .load_data(builtin_costs_ptr, &builtin_costs) .unwrap(); // Load extra data - let core_program_end_ptr = - (runner.program_base.unwrap() + runner.program.shared_program_data.data.len()).unwrap(); + let core_program_end_ptr = (function_runner.program_base.unwrap() + + function_runner.program.shared_program_data.data.len()) + .unwrap(); let program_extra_data: Vec = - vec![0x208B7FFF7FFF7FFE.into(), builtin_costs_ptr.into()]; - runner + vec![0x208B7FFF7FFF7FFE_u64.into(), builtin_costs_ptr.into()]; + function_runner .vm .load_data(core_program_end_ptr, &program_extra_data) .unwrap(); // Load calldata - let calldata_start = runner.vm.add_memory_segment(); - let calldata_end = runner.vm.load_data(calldata_start, args).unwrap(); + let calldata_start = function_runner.vm.add_memory_segment(); + let calldata_end = function_runner.vm.load_data(calldata_start, args).unwrap(); // Create entrypoint_args @@ -280,23 +296,24 @@ fn run_cairo_1_entrypoint_with_run_resources( MaybeRelocatable::from(calldata_start).into(), MaybeRelocatable::from(calldata_end).into(), ]); - let entrypoint_args: Vec<&CairoArg> = entrypoint_args.iter().collect(); // Run contract entrypoint - runner.run_from_entrypoint( - entrypoint_offset, - &entrypoint_args, + let program_segment_size = + function_runner.program.shared_program_data.data.len() + program_extra_data.len(); + function_runner.run( + EntryPoint::Pc(entrypoint_offset), true, - Some(runner.program.shared_program_data.data.len() + program_extra_data.len()), + Some(program_segment_size), hint_processor, + &entrypoint_args, )?; // Check return values - let return_values = runner.vm.get_return_values(5).unwrap(); + let return_values = function_runner.vm.get_return_values(5).unwrap(); let retdata_start = return_values[3].get_relocatable().unwrap(); let retdata_end = return_values[4].get_relocatable().unwrap(); - let retdata: Vec = runner + let retdata: Vec = function_runner .vm .get_integer_range(retdata_start, (retdata_end - retdata_start).unwrap()) .unwrap() diff --git a/vm/src/types/errors/program_errors.rs b/vm/src/types/errors/program_errors.rs index d972a17068..43bc5d3858 100644 --- a/vm/src/types/errors/program_errors.rs +++ b/vm/src/types/errors/program_errors.rs @@ -19,6 +19,10 @@ pub enum ProgramError { StrippedProgramNoMain, #[error("Hint PC ({0}) is greater or equal to program length ({1})")] InvalidHintPc(usize, usize), + #[error("Identifier \"{0}\" is type alias but has no destination")] + AliasMissingDestination(String), + #[error("invalid identifier type \"{1}\" for \"{0}\": expected \"alias\" or \"function\"")] + InvalidIdentifierTypeForPc(String, String), } #[cfg(test)] @@ -31,4 +35,27 @@ mod tests { let formatted_error = format!("{error}"); assert_eq!(formatted_error, "Entrypoint my_function not found"); } + + #[test] + fn format_alias_missing_destination_error() { + let error = ProgramError::AliasMissingDestination(String::from("__main__.assert_nn")); + let formatted_error = format!("{error}"); + assert_eq!( + formatted_error, + "Identifier \"__main__.assert_nn\" is type alias but has no destination" + ); + } + + #[test] + fn format_invalid_identifier_type_for_pc_error() { + let error = ProgramError::InvalidIdentifierTypeForPc( + String::from("__main__.my_struct"), + String::from("struct"), + ); + let formatted_error = format!("{error}"); + assert_eq!( + formatted_error, + "invalid identifier type \"struct\" for \"__main__.my_struct\": expected \"alias\" or \"function\"" + ); + } } diff --git a/vm/src/types/relocatable.rs b/vm/src/types/relocatable.rs index 37d1358624..f360b4e419 100644 --- a/vm/src/types/relocatable.rs +++ b/vm/src/types/relocatable.rs @@ -7,6 +7,7 @@ use crate::Felt252; use crate::{ relocatable, types::errors::math_errors::MathError, vm::errors::memory_errors::MemoryError, }; +use num_bigint::{BigInt, BigUint}; use num_traits::ToPrimitive; use serde::{Deserialize, Serialize}; @@ -58,12 +59,6 @@ impl From<(isize, usize)> for MaybeRelocatable { } } -impl From for MaybeRelocatable { - fn from(num: usize) -> Self { - MaybeRelocatable::Int(Felt252::from(num)) - } -} - impl From for MaybeRelocatable { fn from(num: Felt252) -> Self { MaybeRelocatable::Int(num) @@ -94,6 +89,25 @@ impl From for MaybeRelocatable { } } +// Implement primitive and big-int (owned + reference) conversions by first converting to Felt252, +// then wrapping as MaybeRelocatable::Int. +macro_rules! impl_from_for_maybe_relocatable { + ($($t:ty),* $(,)?) => { + $( + impl From<$t> for MaybeRelocatable { + fn from(num: $t) -> Self { + MaybeRelocatable::Int(Felt252::from(num)) + } + } + )* + }; +} + +impl_from_for_maybe_relocatable!( + u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize, BigUint, BigInt, &BigUint, + &BigInt +); + impl Display for MaybeRelocatable { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -396,6 +410,7 @@ mod tests { use super::*; use crate::{felt_hex, felt_str}; use crate::{relocatable, utils::test_utils::mayberelocatable}; + use num_bigint::{BigInt, BigUint}; use proptest::prelude::*; @@ -429,6 +444,45 @@ mod tests { } } + #[test] + // Verifies primitive integers convert into `MaybeRelocatable::Int` via the macro-generated impls. + fn maybe_relocatable_from_primitive_via_macro() { + let value = MaybeRelocatable::from(42_u8); + assert_eq!(value, MaybeRelocatable::Int(Felt252::from(42_u8))); + } + + #[test] + // Verifies owned `BigUint`/`BigInt` values convert into `MaybeRelocatable::Int`. + fn maybe_relocatable_from_owned_bigints_via_macro() { + let big_uint = BigUint::from(123_u32); + let big_int = BigInt::from(456_i32); + + assert_eq!( + MaybeRelocatable::from(big_uint), + MaybeRelocatable::Int(Felt252::from(123_u32)) + ); + assert_eq!( + MaybeRelocatable::from(big_int), + MaybeRelocatable::Int(Felt252::from(456_i32)) + ); + } + + #[test] + // Verifies referenced `&BigUint`/`&BigInt` values convert into `MaybeRelocatable::Int`. + fn maybe_relocatable_from_referenced_bigints_via_macro() { + let big_uint = BigUint::from(789_u32); + let big_int = BigInt::from(321_i32); + + assert_eq!( + MaybeRelocatable::from(&big_uint), + MaybeRelocatable::Int(Felt252::from(789_u32)) + ); + assert_eq!( + MaybeRelocatable::from(&big_int), + MaybeRelocatable::Int(Felt252::from(321_i32)) + ); + } + #[test] fn add_bigint_to_int() { let addr = MaybeRelocatable::from(Felt252::from(7i32)); diff --git a/vm/src/vm/errors/runner_errors.rs b/vm/src/vm/errors/runner_errors.rs index a9561630e6..0bb0f7f930 100644 --- a/vm/src/vm/errors/runner_errors.rs +++ b/vm/src/vm/errors/runner_errors.rs @@ -124,6 +124,8 @@ pub enum RunnerError { DynamicLayoutLogDilutedUnitsPerStepOverflow(i32), #[error("Initialization failure: Cannot run with trace padding disabled without proof mode")] DisableTracePaddingWithoutProofMode, + #[error("Builtin {0} is not supported in Stwo mode")] + UnsupportedStwoBuiltin(BuiltinName), } #[cfg(test)] diff --git a/vm/src/vm/runners/cairo_function_runner.rs b/vm/src/vm/runners/cairo_function_runner.rs new file mode 100644 index 0000000000..aec0b6df4b --- /dev/null +++ b/vm/src/vm/runners/cairo_function_runner.rs @@ -0,0 +1,333 @@ +//! A Cairo function runner for testing purposes. +//! +//! This module provides [`CairoFunctionRunner`], a high-level interface for running individual +//! Cairo 0 functions with automatic builtin initialization. It allows direct invocation of specific +//! entrypoints with custom arguments. + +use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor; +use crate::hint_processor::hint_processor_definition::HintProcessor; +use crate::serde::deserialize_program::Identifier; +use crate::types::builtin_name::BuiltinName; +use crate::types::errors::program_errors::ProgramError; +use crate::types::instance_definitions::mod_instance_def::ModInstanceDef; +use crate::types::layout::CairoLayoutParams; +use crate::types::layout_name::LayoutName; +use crate::types::program::Program; +use crate::types::relocatable::MaybeRelocatable; +use crate::vm::errors::cairo_run_errors::CairoRunError; +use crate::vm::errors::memory_errors::MemoryError; +use crate::vm::errors::runner_errors::RunnerError; +use crate::vm::errors::vm_errors::VirtualMachineError; +use crate::vm::errors::vm_exception::VmException; +use crate::vm::runners::builtin_runner::{ + BitwiseBuiltinRunner, EcOpBuiltinRunner, HashBuiltinRunner, KeccakBuiltinRunner, + ModBuiltinRunner, OutputBuiltinRunner, PoseidonBuiltinRunner, RangeCheckBuiltinRunner, + SignatureBuiltinRunner, RC_N_PARTS_96, RC_N_PARTS_STANDARD, +}; +use crate::vm::runners::cairo_runner::{CairoArg, CairoRunner}; +use crate::vm::security::verify_secure_runner; + +/// Identifies a Cairo function entrypoint either by function name or by program counter. +pub enum EntryPoint<'a> { + Name(&'a str), + Pc(usize), +} + +/// A runner for executing individual Cairo functions. +/// Used for testing purposes only. +pub struct CairoFunctionRunner { + /// The Cairo runner instance that manages VM execution. + pub runner: CairoRunner, +} + +/// Pushes a builtin runner into `runner.vm.builtin_runners` after converting it with `.into()`. +/// +/// Example expansion: +/// `push_builtin!(runner, HashBuiltinRunner::new(Some(32), true));` +/// becomes: +/// `runner.vm.builtin_runners.push(HashBuiltinRunner::new(Some(32), true).into());` +macro_rules! push_builtin { + ($runner:expr, $builtin:expr) => { + $runner.vm.builtin_runners.push($builtin.into()); + }; +} + +impl std::ops::Deref for CairoFunctionRunner { + type Target = CairoRunner; + + fn deref(&self) -> &Self::Target { + &self.runner + } +} + +impl std::ops::DerefMut for CairoFunctionRunner { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.runner + } +} + +impl CairoFunctionRunner { + /// Creates a new `CairoFunctionRunner`. + /// + /// Initializes a basic `CairoRunner` with: + /// - `LayoutName::plain` + /// - `dynamic_layout_params = None` + /// - `proof_mode = false` + /// - `trace_enabled = false` + /// - `disable_trace_padding = false` + /// + /// and then preloads a fixed set of commonly used builtins. + /// + /// # Arguments + /// - `program`: The compiled Cairo program to execute. + /// + /// # Returns + /// - `Ok(CairoFunctionRunner)`: On successful initialization. + /// - `Err(CairoRunError)`: If the runner cannot be created. + #[allow(clippy::result_large_err)] + pub fn new(program: &Program) -> std::result::Result { + let mut runner = CairoRunner::new( + program, + LayoutName::plain, + None, // dynamic_layout_params + false, // proof_mode + false, // trace_enabled + false, // disable_trace_padding + )?; + + Self::initialize_all_builtins(&mut runner)?; + runner.initialize_segments(None); + + Ok(Self { runner }) + } + /// Creates a new `CairoFunctionRunner` with custom `CairoRunner` initialization parameters. + /// + /// Unlike [`Self::new`], this constructor does not preload builtins or initialize segments. + #[allow(clippy::result_large_err)] + pub fn new_custom( + program: &Program, + layout: LayoutName, + dynamic_layout_params: Option, + proof_mode: bool, + trace_enabled: bool, + disable_trace_padding: bool, + ) -> std::result::Result { + let runner = CairoRunner::new( + program, + layout, + dynamic_layout_params, + proof_mode, + trace_enabled, + disable_trace_padding, + )?; + + Ok(Self { runner }) + } + /// Initializes a fixed set of 11 builtins used by this function runner. + fn initialize_all_builtins(runner: &mut CairoRunner) -> Result<(), RunnerError> { + runner.vm.builtin_runners.clear(); + + push_builtin!(runner, HashBuiltinRunner::new(Some(32), true)); + push_builtin!( + runner, + RangeCheckBuiltinRunner::::new(Some(1), true) + ); + push_builtin!(runner, OutputBuiltinRunner::new(true)); + push_builtin!(runner, SignatureBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, BitwiseBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, EcOpBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, KeccakBuiltinRunner::new(Some(1), true)); + push_builtin!(runner, PoseidonBuiltinRunner::new(Some(1), true)); + push_builtin!( + runner, + RangeCheckBuiltinRunner::::new(Some(1), true) + ); + push_builtin!( + runner, + ModBuiltinRunner::new_add_mod(&ModInstanceDef::new(Some(1), 1, 96), true) + ); + push_builtin!( + runner, + ModBuiltinRunner::new_mul_mod(&ModInstanceDef::new(Some(1), 1, 96), true) + ); + + Ok(()) + } + /// Runs a Cairo function from the specified entrypoint. + /// + /// # Arguments + /// - `entrypoint`: The entrypoint to execute, either by function name or by PC. + /// - `verify_secure`: If `true`, runs additional security verification after execution. + /// - `program_segment_size`: Optional size limit for the program segment. + /// - `program_input`: Optional program input to inject into the execution scopes. + /// - `hint_processor`: The hint processor used during VM execution. + /// - `args`: The function arguments. + /// + /// # Returns + /// - `Ok(())`: On successful execution. + /// - `Err(CairoRunError)`: If the entrypoint is not found, execution fails, or security + /// verification fails. + #[allow(clippy::result_large_err)] + pub fn run( + &mut self, + entrypoint: EntryPoint<'_>, + verify_secure: bool, + program_segment_size: Option, + hint_processor: &mut dyn HintProcessor, + args: &[CairoArg], + ) -> std::result::Result<(), CairoRunError> { + let entrypoint_pc = match entrypoint { + EntryPoint::Name(name) => self.get_function_pc(name)?, + EntryPoint::Pc(pc) => pc, + }; + + let cairo_args: Vec<&CairoArg> = args.iter().collect(); + + self.run_from_entrypoint( + entrypoint_pc, + &cairo_args, + verify_secure, + program_segment_size, + hint_processor, + )?; + + Ok(()) + } + + #[allow(clippy::result_large_err)] + // Builds the call stack from Cairo args, runs until the function's end PC, and optionally verifies security constraints. + fn run_from_entrypoint( + &mut self, + entrypoint: usize, + args: &[&CairoArg], + verify_secure: bool, + program_segment_size: Option, + hint_processor: &mut dyn HintProcessor, + ) -> std::result::Result<(), CairoRunError> { + let stack = args + .iter() + .map(|arg| self.vm.segments.gen_cairo_arg(arg)) + .collect::, VirtualMachineError>>()?; + let return_fp = MaybeRelocatable::from(0_i64); + let end = self.initialize_function_entrypoint(entrypoint, stack, return_fp)?; + + self.initialize_vm()?; + + self.run_until_pc(end, hint_processor) + .map_err(|err| VmException::from_vm_error(self, err))?; + let is_proof_mode = self.is_proof_mode(); + self.end_run(true, false, hint_processor, is_proof_mode)?; + + if verify_secure { + verify_secure_runner(self, false, program_segment_size)?; + } + + Ok(()) + } + + /// Runs a Cairo 0 function with a default empty `BuiltinHintProcessor`for cairo0 files. + #[allow(clippy::result_large_err)] + pub fn run_default_cairo0( + &mut self, + entrypoint: &str, + args: &[CairoArg], + ) -> std::result::Result<(), CairoRunError> { + let mut hint_processor = BuiltinHintProcessor::new_empty(); + self.run( + EntryPoint::Name(entrypoint), + false, + None, + &mut hint_processor, + args, + ) + } + + /// Retrieves return values from the VM's memory after function execution. + /// + /// Reads the last `n_return_values` values from the allocation pointer (AP). + pub fn get_return_values( + &self, + n_return_values: usize, + ) -> Result, MemoryError> { + self.vm.get_return_values(n_return_values) + } + + /// Gets the base pointer for a specific builtin. + pub fn get_builtin_base(&self, builtin_name: BuiltinName) -> Option { + self.vm + .builtin_runners + .iter() + .find(|builtin_runner| builtin_runner.name() == builtin_name) + .map(|builtin_runner| MaybeRelocatable::from((builtin_runner.base() as isize, 0))) + } + + /// Returns the program counter (PC) for a function entrypoint by name. + /// + /// Looks up the identifier `__main__.{entrypoint}` in the program, then resolves it to a PC + /// (following alias chains if needed) via [`get_pc_from_identifier`](Self::get_pc_from_identifier). + /// + /// # Errors + /// - [`ProgramError::EntrypointNotFound`] if no identifier exists for the given name. + /// - [`RunnerError::NoPC`] if the resolved identifier has no PC (e.g. corrupt alias). + /// - [`ProgramError::AliasMissingDestination`] if an alias has no `destination`. + /// - [`ProgramError::InvalidIdentifierTypeForPc`] if the identifier type is not `"function"` or `"alias"`. + #[allow(clippy::result_large_err)] + pub(crate) fn get_function_pc( + &self, + entrypoint: &str, + ) -> std::result::Result { + let full_name = format!("__main__.{entrypoint}"); + let identifier = self + .program + .get_identifier(&full_name) + .ok_or_else(|| ProgramError::EntrypointNotFound(entrypoint.to_string()))?; + + self.get_pc_from_identifier(identifier) + } + + /// Resolves an identifier to its program counter (PC), following alias chains. + /// + /// - **`function`**: returns the identifier's `pc` if present. + /// - **`alias`**: resolves `destination` to another identifier and recurses until a function is found. + /// - **Other types** (e.g. `struct`, `const`): returns [`ProgramError::InvalidIdentifierTypeForPc`]. + /// + /// # Errors + /// - [`RunnerError::NoPC`] when the identifier is a function but has no `pc`. + /// - [`ProgramError::AliasMissingDestination`] when the identifier is an alias but has no `destination`. + /// - [`ProgramError::EntrypointNotFound`] when the alias destination is not in the program. + /// - [`ProgramError::InvalidIdentifierTypeForPc`] when the identifier type is not `"function"` or `"alias"`. + #[allow(clippy::result_large_err)] + fn get_pc_from_identifier( + &self, + idetifier: &Identifier, + ) -> std::result::Result { + match idetifier.type_.as_deref() { + Some("function") => { + let pc = idetifier.pc.ok_or(RunnerError::NoPC)?; + Ok(pc) + } + Some("alias") => { + let destination = idetifier.destination.as_deref().ok_or( + ProgramError::AliasMissingDestination( + idetifier.full_name.as_deref().unwrap_or("").to_string(), + ), + )?; + + let destination_identifier = self + .runner + .program + .get_identifier(destination) + .ok_or_else(|| ProgramError::EntrypointNotFound(destination.to_string()))?; + self.get_pc_from_identifier(destination_identifier) + } + v => { + let name = idetifier + .full_name + .clone() + .unwrap_or_else(|| "".to_string()); + let type_str = v.unwrap_or("").to_string(); + Err(ProgramError::InvalidIdentifierTypeForPc(name, type_str).into()) + } + } + } +} diff --git a/vm/src/vm/runners/cairo_function_runner_test.rs b/vm/src/vm/runners/cairo_function_runner_test.rs new file mode 100644 index 0000000000..ea9ba6fb5f --- /dev/null +++ b/vm/src/vm/runners/cairo_function_runner_test.rs @@ -0,0 +1,289 @@ +// CairoFunctionRunner unit tests. +// Tested functions: new, new_custom, run, run_default_cairo0, get_builtin_base, get_return_values. +use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor; +use crate::types::builtin_name::BuiltinName; +use crate::types::errors::program_errors::ProgramError; +use crate::types::layout_name::LayoutName; +use crate::types::program::Program; +use crate::types::relocatable::MaybeRelocatable; +use crate::vm::errors::cairo_run_errors::CairoRunError; +use crate::vm::runners::cairo_function_runner::{CairoFunctionRunner, EntryPoint}; +use crate::vm::runners::cairo_runner::CairoArg; +use assert_matches::assert_matches; + +fn load_program(program_bytes: &[u8]) -> Program { + Program::from_bytes(program_bytes, None).unwrap() +} + +#[test] +// Test that `new` initializes all 11 builtins, creates program/execution segments, and does not initialize segment_arena. +fn new_initializes_expected_builtin_bases() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = CairoFunctionRunner::new(&program).unwrap(); + + assert_eq!(function_runner.runner.vm.builtin_runners.len(), 11); + let expected_builtins = [ + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::output, + BuiltinName::ecdsa, + BuiltinName::bitwise, + BuiltinName::ec_op, + BuiltinName::keccak, + BuiltinName::poseidon, + BuiltinName::range_check96, + BuiltinName::add_mod, + BuiltinName::mul_mod, + ]; + + for builtin in expected_builtins { + assert!(function_runner.get_builtin_base(builtin).is_some()); + } + assert!(function_runner + .get_builtin_base(BuiltinName::segment_arena) + .is_none()); + assert_eq!(function_runner.runner.vm.segments.num_segments(), 11 + 2); +} + +#[test] +// Test that `new_custom` does not initialize builtins or memory segments automatically. +fn new_custom_does_not_initialize_builtins_or_segments() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = + CairoFunctionRunner::new_custom(&program, LayoutName::plain, None, false, false, false) + .unwrap(); + + assert!(function_runner + .get_builtin_base(BuiltinName::range_check) + .is_none()); + assert_eq!(function_runner.runner.vm.segments.num_segments(), 0); +} + +#[test] +// Test successful function execution by entrypoint name for multiple functions in one program. +fn run_from_entrypoint_custom_program_test() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let range_check_ptr = function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let main_args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(range_check_ptr.clone()), + ]; + assert_matches!( + function_runner.run( + EntryPoint::Name("main"), + true, + None, + &mut hint_processor, + &main_args, + ), + Ok(()) + ); + + let mut second_function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut second_hint_processor = BuiltinHintProcessor::new_empty(); + let second_range_check_ptr = second_function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let fib_args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(second_range_check_ptr), + ]; + assert_matches!( + second_function_runner.run( + EntryPoint::Name("evaluate_fib"), + true, + None, + &mut second_hint_processor, + &fib_args, + ), + Ok(()) + ); +} + +#[test] +// Test successful execution using `EntryPoint::Pc` instead of function name lookup. +fn run_by_program_counter_happy_path() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let range_check_ptr = function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(range_check_ptr), + ]; + let entrypoint_pc = function_runner + .runner + .program + .get_identifier("__main__.main") + .unwrap() + .pc + .unwrap(); + + assert_matches!( + function_runner.run( + EntryPoint::Pc(entrypoint_pc), + true, + None, + &mut hint_processor, + &args, + ), + Ok(()) + ); +} + +#[test] +// Test `run_default_cairo0` happy path and verify zero requested return values. +fn run_default_cairo0_happy_path() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let range_check_ptr = function_runner + .get_builtin_base(BuiltinName::range_check) + .unwrap(); + let args = vec![ + CairoArg::from(MaybeRelocatable::from(2_i64)), + CairoArg::from(range_check_ptr), + ]; + + assert_matches!(function_runner.run_default_cairo0("main", &args), Ok(())); + assert_eq!(function_runner.get_return_values(0).unwrap(), vec![]); +} + +#[test] +// Test that get_function_pc resolves "__main__.assert_nn" (alias) to the PC of starkware.cairo.common.math.assert_nn (0). +fn get_function_pc_assert_nn_resolves_alias_to_pc_0() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = CairoFunctionRunner::new(&program).unwrap(); + + let pc = function_runner.get_function_pc("assert_nn").unwrap(); + assert_eq!( + pc, 0, + "assert_nn is an alias to starkware.cairo.common.math.assert_nn which has pc 0" + ); +} + +#[test] +// Test that get_function_pc returns the direct PC for "__main__.assert_nn_manual_implementation" (function with pc 4). +fn get_function_pc_assert_nn_manual_implementation_returns_pc_4() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let function_runner = CairoFunctionRunner::new(&program).unwrap(); + + let pc = function_runner + .get_function_pc("assert_nn_manual_implementation") + .unwrap(); + assert_eq!( + pc, 4, + "assert_nn_manual_implementation is a function with pc 4" + ); +} + +#[test] +// Test that running a missing function name returns `EntrypointNotFound`. +fn run_missing_entrypoint_returns_entrypoint_not_found() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + + assert_matches!( + function_runner.run( + EntryPoint::Name("missing_entrypoint"), + false, + None, + &mut hint_processor, + &[], + ), + Err(CairoRunError::Program(ProgramError::EntrypointNotFound(entrypoint))) + if entrypoint == "missing_entrypoint" + ); +} + +#[test] +// Test that `run_default_cairo0` propagates missing entrypoint errors. +fn run_default_cairo0_missing_entrypoint_returns_entrypoint_not_found() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/example_program.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + + assert_matches!( + function_runner.run_default_cairo0("missing_entrypoint", &[]), + Err(CairoRunError::Program(ProgramError::EntrypointNotFound(entrypoint))) + if entrypoint == "missing_entrypoint" + ); +} + +#[test] +// Test bitwise builtin execution and verify no memory holes remain. +fn run_from_entrypoint_bitwise_test_check_memory_holes() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/bitwise_builtin_test.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let bitwise_ptr = function_runner + .get_builtin_base(BuiltinName::bitwise) + .unwrap(); + let args = vec![CairoArg::from(bitwise_ptr)]; + + assert!(function_runner + .run( + EntryPoint::Name("main"), + true, + None, + &mut hint_processor, + &args, + ) + .is_ok()); + + assert_eq!(function_runner.runner.get_memory_holes().unwrap(), 0); +} + +#[test] +// Test VM exception error message substitution from `error_msg` attributes. +fn run_from_entrypoint_substitute_error_message_test() { + let program = load_program(include_bytes!( + "../../../../cairo_programs/bad_programs/error_msg_function.json" + )); + let mut function_runner = CairoFunctionRunner::new(&program).unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let result = function_runner.run( + EntryPoint::Name("main"), + true, + None, + &mut hint_processor, + &[], + ); + + match result { + Err(CairoRunError::VmException(exception)) => { + assert_eq!( + exception.error_attr_value, + Some(String::from("Error message: Test error\n")) + ) + } + Err(_) => panic!("Wrong error returned, expected VmException"), + Ok(_) => panic!("Expected run to fail"), + } +} diff --git a/vm/src/vm/runners/cairo_runner.rs b/vm/src/vm/runners/cairo_runner.rs index 58e593a113..0dd41ab8f5 100644 --- a/vm/src/vm/runners/cairo_runner.rs +++ b/vm/src/vm/runners/cairo_runner.rs @@ -30,14 +30,11 @@ use crate::{ utils::is_subsequence, vm::{ errors::{ - cairo_run_errors::CairoRunError, memory_errors::{InsufficientAllocatedCellsError, MemoryError}, runner_errors::RunnerError, trace_errors::TraceError, vm_errors::VirtualMachineError, - vm_exception::VmException, }, - security::verify_secure_runner, { runners::builtin_runner::{ BitwiseBuiltinRunner, BuiltinRunner, EcOpBuiltinRunner, HashBuiltinRunner, @@ -83,18 +80,36 @@ pub enum CairoArg { Composed(Vec), } -impl From for CairoArg { - fn from(other: MaybeRelocatable) -> Self { - CairoArg::Single(other) +// Converts a vector of values into an array-style Cairo argument. +impl From> for CairoArg +where + T: Into, +{ + fn from(other: Vec) -> Self { + CairoArg::Array(other.into_iter().map(Into::into).collect()) } } -impl From> for CairoArg { - fn from(other: Vec) -> Self { - CairoArg::Array(other) +// Converts a single value into a single-item Cairo argument. +impl From for CairoArg +where + T: Into, +{ + fn from(other: T) -> Self { + CairoArg::Single(other.into()) } } +/// Creates a `Vec` from a list of expressions. +/// +/// Each expression is converted using `From for CairoArg`. +#[macro_export] +macro_rules! cairo_args { + ($($x:expr),* $(,)?) => { + vec![$($crate::vm::runners::cairo_runner::CairoArg::from($x)),*] + }; +} + // ================ // RunResources // ================ @@ -243,6 +258,62 @@ impl CairoRunner { }) } + /// Creates a `CairoRunner` for Stwo. + /// Same as `new` but without layout parameters. Builtins are created from + /// an explicit list via `initialize_stwo` instead of being derived from a layout. + /// Must be paired with `initialize_stwo`. Do not use with `initialize`. + pub fn new_stwo( + program: &Program, + mode: RunnerMode, + trace_enabled: bool, + disable_trace_padding: bool, + ) -> Result { + if disable_trace_padding && mode == RunnerMode::ExecutionMode { + return Err(RunnerError::DisableTracePaddingWithoutProofMode); + } + Ok(CairoRunner { + program: program.clone(), + vm: VirtualMachine::new(trace_enabled, disable_trace_padding), + layout: CairoLayout::all_cairo_stwo_instance(), + final_pc: None, + program_base: None, + execution_base: None, + entrypoint: program.shared_program_data.main, + initial_ap: None, + initial_fp: None, + initial_pc: None, + run_ended: false, + segments_finalized: false, + runner_mode: mode.clone(), + relocated_memory: Vec::new(), + exec_scopes: ExecutionScopes::new(), + execution_public_memory: if mode != RunnerMode::ExecutionMode { + Some(Vec::new()) + } else { + None + }, + relocated_trace: None, + }) + } + + /// Initializes the runner in Stwo mode: creates builtins, segments, entrypoint, and VM. + /// Must be used with runners created via `new_stwo`. Do not use with `new`. + pub fn initialize_stwo( + &mut self, + allowed_builtins: &[BuiltinName], + ) -> Result { + self.initialize_builtins_stwo(allowed_builtins)?; + self.initialize_segments(None); + let end = self.initialize_main_entrypoint()?; + for builtin_runner in self.vm.builtin_runners.iter_mut() { + if let BuiltinRunner::Mod(runner) = builtin_runner { + runner.initialize_zero_segment(&mut self.vm.segments); + } + } + self.initialize_vm()?; + Ok(end) + } + pub fn new( program: &Program, layout: LayoutName, @@ -421,7 +492,89 @@ impl CairoRunner { Ok(()) } - fn is_proof_mode(&self) -> bool { + /// Creates builtin runners for Stwo mode. + /// All `allowed_builtins` are created unconditionally with no ratios (dynamic allocation). + /// Program builtins must be a subset of `allowed_builtins`. + /// ECDSA and Keccak are not supported. + fn initialize_builtins_stwo( + &mut self, + allowed_builtins: &[BuiltinName], + ) -> Result<(), RunnerError> { + let allowed: HashSet = allowed_builtins.iter().copied().collect(); + + // Reject unsupported builtins + for name in &[BuiltinName::ecdsa, BuiltinName::keccak] { + if allowed.contains(name) { + return Err(RunnerError::UnsupportedStwoBuiltin(*name)); + } + } + + // Verify program builtins are a subset of allowed builtins + for builtin_name in &self.program.builtins { + if *builtin_name == BuiltinName::segment_arena { + continue; + } + if !allowed.contains(builtin_name) { + return Err(RunnerError::UnsupportedStwoBuiltin(*builtin_name)); + } + } + + // Create builtins in canonical order, all with None ratio. + // In ExecutionMode, only create runners for program builtins (matching + // legacy behavior) so that segment indices are compatible with CairoPie. + let proof_mode = self.is_proof_mode(); + for name in ORDERED_BUILTIN_LIST { + if !allowed.contains(name) { + continue; + } + let included = self.program.builtins.contains(name); + if !proof_mode && !included { + continue; + } + match name { + BuiltinName::output => self + .vm + .builtin_runners + .push(OutputBuiltinRunner::new(included).into()), + BuiltinName::pedersen => self + .vm + .builtin_runners + .push(HashBuiltinRunner::new(None, included).into()), + BuiltinName::range_check => self.vm.builtin_runners.push( + RangeCheckBuiltinRunner::::new(None, included).into(), + ), + BuiltinName::bitwise => self + .vm + .builtin_runners + .push(BitwiseBuiltinRunner::new(None, included).into()), + BuiltinName::ec_op => self + .vm + .builtin_runners + .push(EcOpBuiltinRunner::new(None, included).into()), + BuiltinName::poseidon => self + .vm + .builtin_runners + .push(PoseidonBuiltinRunner::new(None, included).into()), + BuiltinName::range_check96 => self + .vm + .builtin_runners + .push(RangeCheckBuiltinRunner::::new(None, included).into()), + BuiltinName::add_mod => self.vm.builtin_runners.push( + ModBuiltinRunner::new_add_mod(&ModInstanceDef::new(None, 1, 96), included) + .into(), + ), + BuiltinName::mul_mod => self.vm.builtin_runners.push( + ModBuiltinRunner::new_mul_mod(&ModInstanceDef::new(None, 1, 96), included) + .into(), + ), + _ => return Err(RunnerError::UnsupportedStwoBuiltin(*name)), + } + } + + Ok(()) + } + + pub fn is_proof_mode(&self) -> bool { self.runner_mode == RunnerMode::ProofModeCanonical || self.runner_mode == RunnerMode::ProofModeCairo1 } @@ -1171,38 +1324,6 @@ impl CairoRunner { Ok(()) } - #[allow(clippy::result_large_err)] - /// Runs a cairo program from a give entrypoint, indicated by its pc offset, with the given arguments. - /// If `verify_secure` is set to true, [verify_secure_runner] will be called to run extra verifications. - /// `program_segment_size` is only used by the [verify_secure_runner] function and will be ignored if `verify_secure` is set to false. - pub fn run_from_entrypoint( - &mut self, - entrypoint: usize, - args: &[&CairoArg], - verify_secure: bool, - program_segment_size: Option, - hint_processor: &mut dyn HintProcessor, - ) -> Result<(), CairoRunError> { - let stack = args - .iter() - .map(|arg| self.vm.segments.gen_cairo_arg(arg)) - .collect::, VirtualMachineError>>()?; - let return_fp = MaybeRelocatable::from(0); - let end = self.initialize_function_entrypoint(entrypoint, stack, return_fp)?; - - self.initialize_vm()?; - - self.run_until_pc(end, hint_processor) - .map_err(|err| VmException::from_vm_error(self, err))?; - self.end_run(true, false, hint_processor, self.is_proof_mode())?; - - if verify_secure { - verify_secure_runner(self, false, program_segment_size)?; - } - - Ok(()) - } - // Returns Ok(()) if there are enough allocated cells for the builtins. // If not, the number of steps should be increased or a different layout should be used. pub fn check_used_cells(&self) -> Result<(), VirtualMachineError> { @@ -4818,107 +4939,6 @@ mod tests { assert_eq!(bitwise_builtin.stop_ptr, Some(5)); } - #[test] - fn run_from_entrypoint_custom_program_test() { - let program = Program::from_bytes( - include_bytes!("../../../../cairo_programs/example_program.json"), - None, - ) - .unwrap(); - let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - //this entrypoint tells which function to run in the cairo program - let main_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.main") - .unwrap() - .pc - .unwrap(); - - cairo_runner.initialize_builtins(false).unwrap(); - cairo_runner.initialize_segments(None); - assert_matches!( - cairo_runner.run_from_entrypoint( - main_entrypoint, - &[ - &mayberelocatable!(2).into(), - &MaybeRelocatable::from((2, 0)).into() - ], //range_check_ptr - true, - None, - &mut hint_processor, - ), - Ok(()) - ); - - let mut new_cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - new_cairo_runner.initialize_builtins(false).unwrap(); - new_cairo_runner.initialize_segments(None); - - let fib_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.evaluate_fib") - .unwrap() - .pc - .unwrap(); - - assert_matches!( - new_cairo_runner.run_from_entrypoint( - fib_entrypoint, - &[ - &mayberelocatable!(2).into(), - &MaybeRelocatable::from((2, 0)).into() - ], - true, - None, - &mut hint_processor, - ), - Ok(()) - ); - } - - #[test] - fn run_from_entrypoint_bitwise_test_check_memory_holes() { - let program = Program::from_bytes( - include_bytes!("../../../../cairo_programs/bitwise_builtin_test.json"), - None, - ) - .unwrap(); - let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - //this entrypoint tells which function to run in the cairo program - let main_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.main") - .unwrap() - .pc - .unwrap(); - - cairo_runner.initialize_function_runner().unwrap(); - - assert!(cairo_runner - .run_from_entrypoint( - main_entrypoint, - &[ - &MaybeRelocatable::from((2, 0)).into() //bitwise_ptr - ], - true, - None, - &mut hint_processor, - ) - .is_ok()); - - // Check that memory_holes == 0 - assert!(cairo_runner.get_memory_holes().unwrap().is_zero()); - } - #[test] fn cairo_arg_from_single() { let expected = CairoArg::Single(MaybeRelocatable::from((0, 0))); @@ -4933,6 +4953,34 @@ mod tests { assert_eq!(expected, value.into()) } + #[test] + // Verifies converting `Vec` into `CairoArg` produces the expected array argument. + fn cairo_arg_from_vec_of_ints() { + let expected = CairoArg::Array(vec![ + MaybeRelocatable::from(Felt252::from(1)), + MaybeRelocatable::from(Felt252::from(2)), + MaybeRelocatable::from(Felt252::from(3)), + ]); + let value = vec![1_i32, 2_i32, 3_i32]; + assert_eq!(expected, CairoArg::from(value)) + } + + #[test] + // Verifies `cairo_args!` builds a `Vec` from mixed argument types. + fn cairo_args_macro_builds_vec_of_cairo_args() { + let expected = vec![ + CairoArg::Single(MaybeRelocatable::from(Felt252::from(7))), + CairoArg::Single(MaybeRelocatable::from((1, 3))), + CairoArg::Array(vec![ + MaybeRelocatable::from(Felt252::from(9)), + MaybeRelocatable::from(Felt252::from(10)), + ]), + ]; + + let args = crate::cairo_args![7_i32, (1, 3), vec![9_i32, 10_i32],]; + assert_eq!(args, expected); + } + fn setup_execution_resources() -> (ExecutionResources, ExecutionResources) { let mut builtin_instance_counter: BTreeMap = BTreeMap::new(); builtin_instance_counter.insert(BuiltinName::output, 8); @@ -4994,42 +5042,6 @@ mod tests { .contains_key(&BuiltinName::range_check)); } - #[test] - fn run_from_entrypoint_substitute_error_message_test() { - let program = Program::from_bytes( - include_bytes!("../../../../cairo_programs/bad_programs/error_msg_function.json"), - None, - ) - .unwrap(); - let mut cairo_runner = cairo_runner!(program, LayoutName::all_cairo, false, true); - let mut hint_processor = BuiltinHintProcessor::new_empty(); - - //this entrypoint tells which function to run in the cairo program - let main_entrypoint = program - .shared_program_data - .identifiers - .get("__main__.main") - .unwrap() - .pc - .unwrap(); - - cairo_runner.initialize_builtins(false).unwrap(); - cairo_runner.initialize_segments(None); - - let result = - cairo_runner.run_from_entrypoint(main_entrypoint, &[], true, None, &mut hint_processor); - match result { - Err(CairoRunError::VmException(exception)) => { - assert_eq!( - exception.error_attr_value, - Some(String::from("Error message: Test error\n")) - ) - } - Err(_) => panic!("Wrong error returned, expected VmException"), - Ok(_) => panic!("Expected run to fail"), - } - } - #[test] fn get_builtins_final_stack_range_check_builtin() { let program = Program::from_bytes( @@ -5690,4 +5702,224 @@ mod tests { .run_until_pc(end, hint_processor) .expect("failed to run program"); } + + #[test] + fn new_stwo_proof_mode() { + let program = Program::from_bytes( + include_bytes!("../../../../cairo_programs/proof_programs/fibonacci.json"), + Some("main"), + ) + .unwrap(); + let runner = + CairoRunner::new_stwo(&program, RunnerMode::ProofModeCanonical, true, true).unwrap(); + assert_eq!(runner.runner_mode, RunnerMode::ProofModeCanonical); + assert!(runner.execution_public_memory.is_some()); + } + + #[test] + fn new_stwo_execution_mode() { + let program = Program::from_bytes( + include_bytes!("../../../../cairo_programs/fibonacci.json"), + Some("main"), + ) + .unwrap(); + let runner = + CairoRunner::new_stwo(&program, RunnerMode::ExecutionMode, true, false).unwrap(); + assert_eq!(runner.runner_mode, RunnerMode::ExecutionMode); + assert!(runner.execution_public_memory.is_none()); + } + + #[test] + fn new_stwo_disable_trace_padding_without_proof_mode() { + let program = Program::default(); + match CairoRunner::new_stwo(&program, RunnerMode::ExecutionMode, true, true) { + Err(RunnerError::DisableTracePaddingWithoutProofMode) => {} + _ => panic!("Expected DisableTracePaddingWithoutProofMode error"), + } + } + + #[test] + fn initialize_builtins_stwo_creates_all_allowed() { + let program = Program::from_bytes( + include_bytes!("../../../../cairo_programs/proof_programs/fibonacci.json"), + Some("main"), + ) + .unwrap(); + let mut runner = + CairoRunner::new_stwo(&program, RunnerMode::ProofModeCanonical, true, true).unwrap(); + let allowed = vec![ + BuiltinName::output, + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::bitwise, + ]; + runner.initialize_builtins_stwo(&allowed).unwrap(); + let names: Vec = runner.vm.builtin_runners.iter().map(|b| b.name()).collect(); + assert_eq!( + names, + vec![ + BuiltinName::output, + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::bitwise + ] + ); + } + + #[test] + fn initialize_builtins_stwo_rejects_ecdsa() { + let program = Program::default(); + let mut runner = + CairoRunner::new_stwo(&program, RunnerMode::ProofModeCanonical, true, true).unwrap(); + match runner.initialize_builtins_stwo(&[BuiltinName::ecdsa]) { + Err(RunnerError::UnsupportedStwoBuiltin(BuiltinName::ecdsa)) => {} + _ => panic!("Expected UnsupportedStwoBuiltin(ecdsa) error"), + } + } + + #[test] + fn initialize_builtins_stwo_rejects_keccak() { + let program = Program::default(); + let mut runner = + CairoRunner::new_stwo(&program, RunnerMode::ProofModeCanonical, true, true).unwrap(); + match runner.initialize_builtins_stwo(&[BuiltinName::keccak]) { + Err(RunnerError::UnsupportedStwoBuiltin(BuiltinName::keccak)) => {} + _ => panic!("Expected UnsupportedStwoBuiltin(keccak) error"), + } + } + + #[test] + fn initialize_builtins_stwo_rejects_program_builtin_not_in_allowed() { + let program = Program::from_bytes( + include_bytes!("../../../../cairo_programs/proof_programs/bitwise_builtin_test.json"), + Some("main"), + ) + .unwrap(); + let mut runner = + CairoRunner::new_stwo(&program, RunnerMode::ProofModeCanonical, true, true).unwrap(); + // bitwise_builtin_test requires bitwise, but we only allow output + match runner.initialize_builtins_stwo(&[BuiltinName::output]) { + Err(RunnerError::UnsupportedStwoBuiltin(BuiltinName::bitwise)) => {} + _ => panic!("Expected UnsupportedStwoBuiltin(bitwise) error"), + } + } + + #[test] + fn cairo_run_stwo_fibonacci() { + let program = Program::from_bytes( + include_bytes!("../../../../cairo_programs/proof_programs/fibonacci.json"), + Some("main"), + ) + .unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let allowed = vec![ + BuiltinName::output, + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::bitwise, + BuiltinName::ec_op, + BuiltinName::poseidon, + ]; + let runner = crate::cairo_run::cairo_run_stwo( + &program, + RunnerMode::ProofModeCanonical, + &allowed, + &mut hint_processor, + ExecutionScopes::new(), + &crate::cairo_run::StwoCairoRunConfig::default(), + ) + .unwrap(); + assert!(runner.relocated_trace.is_some()); + } + + #[test] + fn cairo_run_stwo_with_builtins() { + let program = Program::from_bytes( + include_bytes!("../../../../cairo_programs/proof_programs/bitwise_builtin_test.json"), + Some("main"), + ) + .unwrap(); + let mut hint_processor = BuiltinHintProcessor::new_empty(); + let allowed = vec![ + BuiltinName::output, + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::bitwise, + BuiltinName::ec_op, + BuiltinName::poseidon, + ]; + let runner = crate::cairo_run::cairo_run_stwo( + &program, + RunnerMode::ProofModeCanonical, + &allowed, + &mut hint_processor, + ExecutionScopes::new(), + &crate::cairo_run::StwoCairoRunConfig::default(), + ) + .unwrap(); + assert!(runner.relocated_trace.is_some()); + } + + #[test] + fn cairo_run_stwo_matches_legacy_all_cairo_stwo() { + let programs: &[&[u8]] = &[ + include_bytes!("../../../../cairo_programs/proof_programs/fibonacci.json"), + include_bytes!("../../../../cairo_programs/proof_programs/bitwise_builtin_test.json"), + ]; + // Match the all_cairo_stwo layout builtins (mod builtins excluded + // unless the mod_builtin feature is enabled). + let mut allowed = vec![ + BuiltinName::output, + BuiltinName::pedersen, + BuiltinName::range_check, + BuiltinName::bitwise, + BuiltinName::ec_op, + BuiltinName::poseidon, + BuiltinName::range_check96, + ]; + if cfg!(feature = "mod_builtin") { + allowed.push(BuiltinName::add_mod); + allowed.push(BuiltinName::mul_mod); + } + for program_bytes in programs { + let program = Program::from_bytes(program_bytes, Some("main")).unwrap(); + + // Legacy run with all_cairo_stwo layout + let legacy_runner = crate::cairo_run::cairo_run( + program_bytes, + &CairoRunConfig { + trace_enabled: true, + relocate_mem: true, + relocate_trace: true, + layout: LayoutName::all_cairo_stwo, + proof_mode: true, + disable_trace_padding: true, + ..Default::default() + }, + &mut BuiltinHintProcessor::new_empty(), + ) + .unwrap(); + + // Stwo run + let stwo_runner = crate::cairo_run::cairo_run_stwo( + &program, + RunnerMode::ProofModeCanonical, + &allowed, + &mut BuiltinHintProcessor::new_empty(), + ExecutionScopes::new(), + &crate::cairo_run::StwoCairoRunConfig { + trace_enabled: true, + relocate_mem: true, + relocate_trace: true, + fill_holes: false, + secure_run: true, + disable_trace_padding: true, + }, + ) + .unwrap(); + + assert_eq!(legacy_runner.relocated_memory, stwo_runner.relocated_memory); + assert_eq!(legacy_runner.relocated_trace, stwo_runner.relocated_trace); + } + } } diff --git a/vm/src/vm/runners/mod.rs b/vm/src/vm/runners/mod.rs index ed5ecaa7dc..96f93f8989 100644 --- a/vm/src/vm/runners/mod.rs +++ b/vm/src/vm/runners/mod.rs @@ -1,3 +1,6 @@ pub mod builtin_runner; +pub mod cairo_function_runner; +#[cfg(test)] +mod cairo_function_runner_test; pub mod cairo_pie; pub mod cairo_runner;