diff --git a/.github/actions/build-mips/action.yaml b/.github/actions/build-mips/action.yaml new file mode 100644 index 0000000000..26dda82f20 --- /dev/null +++ b/.github/actions/build-mips/action.yaml @@ -0,0 +1,22 @@ +name: 'Build MIPS Programs' +description: 'Builds MIPS programs for testing' + +runs: + using: "composite" + steps: + - name: Cache apt packages + uses: actions/cache@v4 + with: + path: | + /var/cache/apt/archives/*.deb + key: ${{ runner.os }}-apt-${{ hashFiles('.github/workflows/o1vm-mips-build.yml') }} + + - name: Install MIPS tools + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y binutils-mips-linux-gnu + + - name: Build MIPS programs + shell: bash + run: make build-mips-programs \ No newline at end of file diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 8da92c5de5..3d4434b538 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -52,6 +52,9 @@ jobs: run: | make install-test-deps + - name: Build the MIPS binaries + uses: ./.github/actions/build-mips + - name: Run all tests with the code coverage run: | eval $(opam env) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09b91f2ab9..97502dd7a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,6 +162,9 @@ jobs: run: | make install-test-deps + - name: Build the MIPS binaries + uses: ./.github/actions/build-mips + - name: Doc tests if: ${{ matrix.rust_toolchain_version != env.RUST_TOOLCHAIN_COVERAGE_VERSION }} run: | diff --git a/.github/workflows/mips-build.yml b/.github/workflows/mips-build.yml deleted file mode 100644 index 264a485ee4..0000000000 --- a/.github/workflows/mips-build.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: MIPS Build and Package - -on: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - rust_toolchain_version: ["1.74"] - - steps: - - uses: actions/checkout@v4 - with: - submodules: 'recursive' - - - name: Cache apt packages - id: apt-cache - uses: actions/cache@v4 - with: - path: | - /var/cache/apt/archives/*.deb - key: ${{ runner.os }}-apt-${{ hashFiles('.github/workflows/mips-build.yml') }} - - - name: Install MIPS tools - run: | - sudo apt-get update - sudo apt-get install -y binutils-mips-linux-gnu - - - name: Build MIPS programs - run: make build-mips-programs - - - name: Use shared Rust toolchain setting up steps - uses: ./.github/actions/toolchain-shared - with: - rust_toolchain_version: ${{ matrix.rust_toolchain_version }} - - - name: Test elf_loader against mips programs - run: ./o1vm/test-gen-state-json.sh - - - name: Create tar archive - run: | - cd o1vm/resources/programs/mips - tar -czf mips-binaries.tar.gz bin/ - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: mips-binaries - path: o1vm/resources/programs/mips/mips-binaries.tar.gz diff --git a/.github/workflows/o1vm-upload-mips-build.yml b/.github/workflows/o1vm-upload-mips-build.yml new file mode 100644 index 0000000000..72e3e77cb6 --- /dev/null +++ b/.github/workflows/o1vm-upload-mips-build.yml @@ -0,0 +1,28 @@ +name: Upload MIPS Binaries + +on: + workflow_dispatch: + +jobs: + build_and_upload: + name: Build and Upload MIPS Programs + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v4 + with: + submodules: 'recursive' + + - name: Build MIPS binaries + uses: ./.github/actions/build-mips + + - name: Create tar archive + run: | + cd o1vm/resources/programs/mips + tar -czf mips-binaries.tar.gz bin/ + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: mips-binaries + path: o1vm/resources/programs/mips/mips-binaries.tar.gz \ No newline at end of file diff --git a/Makefile b/Makefile index 43f57c5021..1e6ff2f289 100644 --- a/Makefile +++ b/Makefile @@ -181,7 +181,7 @@ ${O1VM_MIPS_SOURCE_DIR}/%.asm: ${OPTIMISM_MIPS_SOURCE_DIR}/%.asm @echo "Transforming $< to $@, making it compatible for o1vm" @sed \ -e '/\.balign 4/d' \ - -e '/\.set\s*noreorder/d' \ + -e 's/^\s*\.set\s*noreorder/.set noreorder/' \ -e '/\.ent\s*test/d' \ -e '/\.end test/d' \ -e 's/\.section .test, "x"/.section .text/' \ diff --git a/o1vm/Cargo.toml b/o1vm/Cargo.toml index 574a112d1b..0e377143ef 100644 --- a/o1vm/Cargo.toml +++ b/o1vm/Cargo.toml @@ -16,6 +16,9 @@ path = "src/lib.rs" name = "pickles_o1vm" path = "src/pickles/main.rs" +[features] +open_mips = [] + [dependencies] # FIXME: Only activate this when legacy_o1vm is built ark-bn254.workspace = true diff --git a/o1vm/src/cannon.rs b/o1vm/src/cannon.rs index a87e5e92a4..a888bf898d 100644 --- a/o1vm/src/cannon.rs +++ b/o1vm/src/cannon.rs @@ -217,9 +217,29 @@ pub struct VmConfiguration { pub proof_fmt: String, pub snapshot_fmt: String, pub pprof_cpu: bool, + pub halt_address: Option, pub host: Option, } +impl Default for VmConfiguration { + fn default() -> Self { + VmConfiguration { + input_state_file: "state.json".to_string(), + output_state_file: "out.json".to_string(), + metadata_file: None, + proof_at: StepFrequency::Never, + stop_at: StepFrequency::Never, + snapshot_state_at: StepFrequency::Never, + info_at: StepFrequency::Never, + proof_fmt: "proof-%d.json".to_string(), + snapshot_fmt: "state-%d.json".to_string(), + pprof_cpu: false, + halt_address: None, + host: None, + } + } +} + #[derive(Debug, Clone)] pub struct Start { pub time: std::time::Instant, diff --git a/o1vm/src/cli/cannon.rs b/o1vm/src/cli/cannon.rs index 9445be62d0..a8f8277550 100644 --- a/o1vm/src/cli/cannon.rs +++ b/o1vm/src/cli/cannon.rs @@ -61,6 +61,13 @@ pub struct MipsVmConfigurationArgs { )] snapshot_state_at: StepFrequency, + #[arg( + long = "halt-address", + value_name = "ADDR", + help = "halt address (in hexadecimal). Jumping to this address will halt the program." + )] + halt_address: Option, + #[arg(name = "host", value_name = "HOST", help = "host program specification [host program arguments]", num_args = 1.., last = true)] host: Vec, } @@ -78,6 +85,10 @@ impl From for VmConfiguration { proof_fmt: cfg.proof_fmt, snapshot_fmt: cfg.snapshot_fmt, pprof_cpu: cfg.pprof_cpu, + halt_address: cfg.halt_address.map(|s| { + u32::from_str_radix(s.trim_start_matches("0x"), 16) + .expect("Failed to parse halt address as hex") + }), host: if cfg.host.is_empty() { None } else { diff --git a/o1vm/src/interpreters/mips/witness.rs b/o1vm/src/interpreters/mips/witness.rs index b5fa8161a0..eb7db1b967 100644 --- a/o1vm/src/interpreters/mips/witness.rs +++ b/o1vm/src/interpreters/mips/witness.rs @@ -1168,9 +1168,16 @@ impl Env { self.instruction_counter = self.next_instruction_counter(); + config.halt_address.iter().for_each(|halt_address: &u32| { + if self.get_instruction_pointer() == (*halt_address as u64) { + debug!("Program jumped to halt address: {:#X}", halt_address); + self.halt = true; + } + }); + // Integer division by MAX_ACC to obtain the actual instruction count if self.halt { - println!( + debug!( "Halted at step={} instruction={:?}", self.normalized_instruction_counter(), opcode diff --git a/o1vm/test-gen-state-json.sh b/o1vm/test-gen-state-json.sh deleted file mode 100755 index 61a4387c7c..0000000000 --- a/o1vm/test-gen-state-json.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -BIN_DIR=${1:-${BIN_DIR:-o1vm/resources/programs/mips/bin}} - -# Ensure the directory exists -if [[ ! -d "$BIN_DIR" ]]; then - echo "Error: Directory '$BIN_DIR' does not exist." - exit 1 -fi - -find "$BIN_DIR" -type f ! -name "*.o" | while read -r file; do - echo "Processing: $file" - cargo run --bin pickles_o1vm -- cannon gen-state-json -i "$file" -done diff --git a/o1vm/tests/test_mips_elf.rs b/o1vm/tests/test_mips_elf.rs new file mode 100644 index 0000000000..04ec89d336 --- /dev/null +++ b/o1vm/tests/test_mips_elf.rs @@ -0,0 +1,112 @@ +use ark_ff::Field; +use mina_curves::pasta::Fp; +use o1vm::{ + cannon::{self, State, VmConfiguration}, + elf_loader::Architecture, + interpreters::mips::witness::{self}, + preimage_oracle::{NullPreImageOracle, PreImageOracleT}, +}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +struct MipsTest { + bin_file: PathBuf, +} + +// currently excluding any oracle based tests and a select group of tests that are failing +fn is_test_excluded(bin_file: &Path) -> bool { + let file_name = bin_file.file_name().unwrap().to_str().unwrap(); + let untested_programs = ["exit_group", "mul"]; + file_name.starts_with("oracle") || untested_programs.contains(&file_name) +} + +impl MipsTest { + fn parse_state(&self) -> State { + let curr_dir = std::env::current_dir().unwrap(); + let path = curr_dir.join(&self.bin_file); + o1vm::elf_loader::parse_elf(Architecture::Mips, &path).unwrap() + } + + fn read_word(env: &mut witness::Env, addr: u32) -> u32 { + let bytes: [u8; 4] = [ + env.get_memory_direct(addr), + env.get_memory_direct(addr + 1), + env.get_memory_direct(addr + 2), + env.get_memory_direct(addr + 3), + ]; + u32::from_be_bytes(bytes) + } + + fn run(&self) -> Result<(), String> { + println!("Running test: {:?}", self.bin_file); + let mut state = self.parse_state(); + let halt_address = 0xa7ef00d0_u32; + state.registers[31] = halt_address; + + let start = cannon::Start::create(state.step as usize); + let configuration = VmConfiguration { + halt_address: Some(halt_address), + stop_at: cannon::StepFrequency::Exactly(1000), + ..Default::default() + }; + + let mut witness = witness::Env::>::create( + cannon::PAGE_SIZE as usize, + state, + Box::new(NullPreImageOracle), + ); + + while !witness.halt { + witness.step(&configuration, &None, &start); + } + + let return_register = 0xbffffff0_u32; + let done_register = return_register + 4; + let result_register = return_register + 8; + + let done_value = Self::read_word(&mut witness, done_register); + if done_value != 1 { + return Err(format!( + "Expected done register to be set to 1, got {:#x}", + done_value + )); + } + + let result_value = Self::read_word(&mut witness, result_register); + if result_value != 1 { + return Err(format!( + "Program {:?} failure: expected result register to contain 1, got {:#x}", + self.bin_file, result_value + )); + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg_attr(not(feature = "open_mips"), ignore)] + fn open_mips_tests() { + let test_dir = "resources/programs/mips/bin"; + let test_files: Vec = fs::read_dir(test_dir) + .unwrap_or_else(|_| panic!("Error reading directory {}", test_dir)) + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + .filter(|f| f.is_file() && f.extension().is_none() && !is_test_excluded(f)) + .map(|f| MipsTest { bin_file: f }) + .collect(); + + for test in test_files { + let test_name = test.bin_file.file_name().unwrap().to_str().unwrap(); + if let Err(err) = test.run() { + panic!("Test '{}' failed: {}", test_name, err); + } + } + } +}