Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert NVM Back to RV32 Base #164

Merged
merged 47 commits into from
May 30, 2024
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
9458b66
Restore old circuits.
sjudson May 8, 2024
3399b29
Segment out Nexus extension.
sjudson May 9, 2024
249e609
Move everything together, without the pieces tied up internally.
sjudson May 9, 2024
09c9ff3
Fix some dependencies and restore all memory interface.
sjudson May 9, 2024
d0031d9
More reconciliation work.
sjudson May 9, 2024
a2ce1e4
Plodding along with reconciliation.
sjudson May 9, 2024
4827f98
Still more yet todo + some formatting.
sjudson May 9, 2024
c6583dc
More conversion.
sjudson May 9, 2024
ec91bd5
Maybe getting there.
sjudson May 10, 2024
686289a
Get compiling.
sjudson May 13, 2024
569e68a
Fix proofs and get tests passing.
sjudson May 13, 2024
b408268
Clippy + formatting.
sjudson May 13, 2024
2cf87da
Large amounts of integration work.
sjudson May 14, 2024
02690ad
Getting there with unit tests.
sjudson May 14, 2024
f237a3d
Format and clippy.
sjudson May 14, 2024
b789e96
Minor.
sjudson May 14, 2024
05bf956
Use paged memory for cli.
sjudson May 14, 2024
68e7640
Restore reading from private input tape.
sjudson May 14, 2024
3dfa8a6
Working towards resolving memory proofs.
sjudson May 14, 2024
4767256
Fix proof size inconsistency.
sjudson May 14, 2024
b7c1a1f
Minor.
sjudson May 15, 2024
6b621bf
Minor.
sjudson May 15, 2024
c97369c
Start to reshape circuit for ecall.
sjudson May 21, 2024
e841e81
Some cleaning
sjudson May 22, 2024
4a88a75
Minor.
sjudson May 23, 2024
b90d2ad
Get working by updating opcode.
sjudson May 28, 2024
d813f75
Fix.
sjudson May 28, 2024
d062c7b
Fix display issue.
sjudson May 28, 2024
38edd94
Format.
sjudson May 28, 2024
0a39c97
Restore documentation.
sjudson May 28, 2024
90c051f
More documentation.
sjudson May 28, 2024
5375f76
Format.
sjudson May 28, 2024
447f228
Clippy.
sjudson May 28, 2024
d685995
Fix test.
sjudson May 28, 2024
d4aa98e
Update vm/src/error.rs
sjudson May 29, 2024
0d07baa
Remove mistake from rebase.
sjudson May 29, 2024
556a4f5
More rebase fixing.
sjudson May 29, 2024
48d9aa2
More on rebasing with Jolt.
sjudson May 29, 2024
506f9c1
Update vm/src/rv32/parse.rs
sjudson May 29, 2024
9090eae
Finish Jolt rebase.
sjudson May 29, 2024
0323018
Add runtime check for instruction set.
sjudson May 29, 2024
bc82783
Remove comment.
sjudson May 29, 2024
d130aa2
Format.
sjudson May 29, 2024
b922877
Match all rd values.
sjudson May 29, 2024
adc3886
Formatting.
sjudson May 29, 2024
b623018
Fix tracking of branching instructions for larger k.
sjudson May 30, 2024
f60ef84
Formatting.
sjudson May 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ resolver = "2"
members = [
"runtime",
"examples",
"riscv",
"vm",
"tools",
"tools/tools-dev",
@@ -18,7 +17,6 @@ members = [
"jolt",
]
default-members = [
"riscv",
"vm",
"tools",
"prover",
1 change: 0 additions & 1 deletion api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -11,6 +11,5 @@ categories = { workspace = true }

[dependencies]
nexus-config = { path = "../config" }
nexus-riscv = { path = "../riscv" }
nexus-vm = { path = "../vm" }
nexus-prover = { path = "../prover" }
25 changes: 12 additions & 13 deletions api/examples/nvm_run.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
// An example of loading and running the NVM.

use nexus_api::{
nvm::{
self,
memory::{MerkleTrie, Paged},
NexusVM,
},
riscv::{self, run_as_nvm, VMOpts},
use nexus_api::nvm::{
self,
memory::{MerkleTrie, Paged},
run_vm, NexusVM, VMOpts,
};
use std::path::PathBuf;

@@ -19,11 +16,13 @@ fn main() {
file: None,
};

run_as_nvm::<MerkleTrie>(&opts, true, true).expect("error running Nexus VM");
run_vm::<MerkleTrie>(&opts, true).expect("error running Nexus VM");

// For this example we are using a built-in test VM, but using paged memory.
run_as_nvm::<Paged>(&opts, true, true).expect("error running Nexus VM");
let pb = PathBuf::from(r"../target/riscv32i-unknown-none-elf/debug/fib");
run_vm::<Paged>(&opts, true).expect("error running Nexus VM");

// expects example programs (`nexus-zkvm/examples`) to have been built with `cargo build -r`
let pb = PathBuf::from(r"../target/riscv32i-unknown-none-elf/release/fib");

// For this example we are using an ELF file, accessed through the single-entry interface.
let opts = VMOpts {
@@ -32,10 +31,10 @@ fn main() {
file: Some(pb.clone()),
};

run_as_nvm::<MerkleTrie>(&opts, true, true).expect("error running Nexus VM");
run_vm::<MerkleTrie>(&opts, true).expect("error running Nexus VM");

// For this example we are using an ELF file, accessed through the interactive interface.
let mut vm: NexusVM<MerkleTrie> =
riscv::interactive::translate_elf(&pb).expect("error loading and translating RISC-V VM");
let mut vm: NexusVM<Paged> =
nvm::interactive::load_elf(&pb).expect("error loading and parsing RISC-V VM");
nvm::interactive::eval(&mut vm, true).expect("error running Nexus VM");
}
7 changes: 4 additions & 3 deletions api/examples/prover_run.rs
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ use nexus_api::{
config::vm::{ProverImpl, VmConfig},
nvm::{self, memory::MerkleTrie, NexusVM},
prover::{self},
riscv::{self},
};
use nexus_config::vm::NovaImpl;
use std::path::PathBuf;
@@ -18,13 +17,15 @@ fn main() {
// expects example programs (`nexus-zkvm/examples`) to have been built with `cargo build -r`
let pb = PathBuf::from(r"../target/riscv32i-unknown-none-elf/release/private_input");

// nb: the tracing and proving infrastructure assumes use of MerkleTrie memory model

println!("Setting up public parameters...");
let public_params =
prover::setup::gen_vm_pp(CONFIG.k, &()).expect("error generating public parameters");

println!("Reading and translating vm...");
let mut vm: NexusVM<MerkleTrie> =
riscv::interactive::translate_elf(&pb).expect("error loading and translating RISC-V VM");
nvm::interactive::load_elf(&pb).expect("error loading and parsing RISC-V instruction");

vm.syscalls.set_input(&[0x06]);

@@ -38,7 +39,7 @@ fn main() {
.expect("error generating execution trace");
println!("<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");

println!("Proving execution...");
println!("Proving execution of length {}...", trace.blocks.len());
let proof = prover::prove::prove_seq(&public_params, trace).expect("error proving execution");

print!("Verifying execution...");
30 changes: 0 additions & 30 deletions api/examples/riscv_run.rs

This file was deleted.

12 changes: 2 additions & 10 deletions api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -10,19 +10,11 @@ pub mod config {
}

/// RISC-V processing
pub mod riscv {
pub mod interactive {
pub use nexus_riscv::{eval, load_elf, nvm::translate_elf, parse_elf};
}
pub use nexus_riscv::{error::VMError, nvm::run_as_nvm, run_vm, VMOpts};
}

/// Nexus VM
pub mod nvm {
pub mod interactive {
pub use nexus_vm::{eval::eval, trace::trace};
pub use nexus_vm::{eval, load_elf, parse_elf, trace::trace};
}
pub use nexus_vm::{error::NexusVMError, eval::NexusVM};
pub use nexus_vm::{error::NexusVMError, eval::NexusVM, run_vm, trace_vm, VMOpts};
pub mod memory {
pub use nexus_vm::memory::{paged::Paged, trie::MerkleTrie};
}
1 change: 0 additions & 1 deletion jolt/Cargo.toml
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ version = "0.1.0"
edition = "2021"

[dependencies]
nexus-riscv = { path = "../riscv" }
nexus-vm = { path = "../vm" }

tracing = { version = "0.1", default-features = false }
8 changes: 4 additions & 4 deletions jolt/src/convert.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Conversion between NexusVM and Jolt types.
use nexus_riscv::rv32::{Inst, RV32};
use nexus_vm::rv32::{Inst, RV32};

use jolt_common::rv_trace as jolt_rv;

@@ -19,7 +19,7 @@ pub fn inst(inst: Inst) -> jolt_rv::ELFInstruction {

pub fn rv32_opcode(inst: RV32) -> jolt_rv::RV32IM {
use jolt_rv::RV32IM as JoltRV32IM;
use nexus_riscv::rv32::{AOP::*, BOP::*, LOP::*, RV32::*, SOP::*};
use nexus_vm::rv32::{AOP::*, BOP::*, LOP::*, RV32::*, SOP::*};

match inst {
LUI { .. } => JoltRV32IM::LUI,
@@ -67,8 +67,8 @@ pub fn rv32_opcode(inst: RV32) -> jolt_rv::RV32IM {
ALU { aop: AND, .. } => JoltRV32IM::AND,

FENCE => JoltRV32IM::FENCE,
ECALL => JoltRV32IM::ECALL,
EBREAK => JoltRV32IM::EBREAK,
ECALL { .. } => JoltRV32IM::ECALL,
EBREAK { .. } => JoltRV32IM::EBREAK,
UNIMP => JoltRV32IM::UNIMPL,
}
}
7 changes: 2 additions & 5 deletions jolt/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use nexus_riscv::{rv32::RV32, VMError};
use nexus_vm::{rv32::RV32, NexusVMError};

use jolt_core::utils::errors::ProofVerifyError;

@@ -7,14 +7,11 @@ use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
VM(#[from] VMError),
VM(#[from] NexusVMError),

#[error("Instruction isn't supported: {0}")]
Unsupported(RV32),

#[error(transparent)]
ProofVerify(#[from] ProofVerifyError),

#[error("memory access")]
Memory(#[from] nexus_vm::error::NexusVMError),
}
10 changes: 6 additions & 4 deletions jolt/src/lib.rs
Original file line number Diff line number Diff line change
@@ -17,6 +17,8 @@ use jolt_core::{
utils::thread::unsafe_allocate_zero_vec,
};

use nexus_vm::{eval::NexusVM, memory::Memory};

use rayon::{
iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
@@ -40,9 +42,9 @@ pub mod trace;
pub use error::Error;

/// Wrapper for initialized VM.
pub struct VM {
pub struct VM<M: Memory> {
/// Initialized Nexus VM.
vm: nexus_riscv::eval::VM,
vm: NexusVM<M>,

/// Instructions section.
insts: Vec<jolt_rv::ELFInstruction>,
@@ -51,13 +53,13 @@ pub struct VM {
mem_init: Vec<(u64, u8)>,
}

impl VM {
impl<M: Memory> VM<M> {
pub fn bytecode_size(&self) -> usize {
self.insts.len()
}
}

pub fn preprocess(vm: &VM) -> JoltPreprocessing {
pub fn preprocess<M: Memory>(vm: &VM<M>) -> JoltPreprocessing {
const MAX_BYTECODE_SIZE: usize = 0x400000;
const MAX_MEMORY_ADDRESS: usize = 1 << 20;
const MAX_TRACE_LENGTH: usize = 1 << 22;
27 changes: 16 additions & 11 deletions jolt/src/parse.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
//! Utilities for parsing elf files.
use nexus_riscv::elf::{
use nexus_vm::elf::{
abi::{PT_LOAD, SHF_ALLOC, SHF_EXECINSTR, SHT_PROGBITS},
endian::LittleEndian,
ElfBytes,
};
use nexus_riscv::{
init_vm, parse_elf_bytes,
use nexus_vm::{
init_vm,
memory::Memory,
parse_elf_bytes,
rv32::{parse::parse_inst, Inst, RV32},
};

use crate::{convert, Error, LOG_TARGET, VM};

pub fn parse_elf(bytes: &[u8]) -> Result<VM, Error> {
pub fn parse_elf<M: Memory>(bytes: &[u8]) -> Result<VM<M>, Error> {
let elf = parse_elf_bytes(bytes)?;

let vm = init_vm(&elf, bytes)?;
@@ -82,13 +84,16 @@ fn parse_instructions(elf: &ElfBytes<LittleEndian>, data: &[u8]) -> Result<Vec<I
});

// UNIMP instruction is OK as long as it's not executed.
if inst.inst == RV32::ECALL || inst.inst == RV32::EBREAK {
tracing::debug!(
target: LOG_TARGET,
?addr,
"Unsupported instruction",
);
return Err(Error::Unsupported(inst.inst));
match inst.inst {
RV32::ECALL { .. } | RV32::EBREAK { .. } => {
tracing::debug!(
target: LOG_TARGET,
?addr,
"Unsupported instruction",
);
return Err(Error::Unsupported(inst.inst));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on a discussion with @sjudson, I anticipated the Jolt prover wouldn't fully work yet, at least not with ECALLs, but I also wanted to test and document the latest behavior.

I tried out the Jolt prover using the steps described at #159 (comment), except with --impl jolt since it's no longer the default, and on the HEAD of this branch instead, currently adc3886.

The default "Hello, World" program fails with the following error:

$ cargo nexus prove --impl jolt
   Compiling proc-macro2 v1.0.84
   Compiling unicode-ident v1.0.12
   Compiling syn v1.0.109
   Compiling nexus-rt v1.0.0 (.../nexus-zkvm/only-riscv/runtime)
   Compiling quote v1.0.36
   Compiling nexus-rt-macros v0.1.0 (.../nexus-zkvm/only-riscv/runtime/macros)
   Compiling pr-164 v0.1.0 (/private/tmp/pr-164)
    Finished release-unoptimized [unoptimized] target(s) in 3.03s
Error: Instruction isn't supported: ecall x10

Based on the error message, it must be coming from the highlighted line. Again, this isn't surprising and on its own doesn't necessarily warrant blocking the PR since Jolt isn't the default prover.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I removed any source of ECALLs in the program to prove/verify, the jolt impl worked as expected. I've not audited to see what, if anything, the optimizer elided as dead code, but it's good to see there's no apparent regression here.

diff --git a/src/main.rs b/src/main.rs
index 69a7814..f271880 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,5 +5,8 @@ use nexus_rt::write_log;
 
 #[nexus_rt::main]
 fn main() {
-    write_log("Hello, World!\n");
+    let mut i = 0;
+    while i < 10 {
+        i += 1;
+    }
 }

Prove then verify with --impl jolt:

$ cargo nexus prove --impl jolt
   Compiling proc-macro2 v1.0.84
   Compiling unicode-ident v1.0.12
   Compiling syn v1.0.109
   Compiling nexus-rt v1.0.0 (.../nexus-zkvm/only-riscv/runtime)
   Compiling quote v1.0.36
   Compiling nexus-rt-macros v0.1.0 (.../nexus-zkvm/only-riscv/runtime/macros)
   Compiling pr-164 v0.1.0 (/private/tmp/pr-164)
warning: unused import: `nexus_rt::write_log`
...
    Finished release-unoptimized [unoptimized] target(s) in 2.64s
  Preprocessing bytecode ... 533ms
       Finished in 533ms; bytecode size: 55
Executing program...
Executed 104 instructions in 8.011541ms
  Proving program execution ... 2.5s
 Finished in 2.5s
  Saving proof ... 1ms
Finished in 1ms

$ cargo nexus verify --impl jolt
  Preprocessing bytecode ... 480ms
       Finished in 480ms; bytecode size: 55
  Verifying proof ... 143ms
   Finished in 143ms

}
_ => (),
}

insts.push(inst);
59 changes: 31 additions & 28 deletions jolt/src/trace.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,38 @@
use nexus_riscv::{
eval::{add32, eval_inst, VM as NexusVM},
rv32::{parse::parse_inst, Inst, RV32},
use nexus_vm::{
eval::{add32, eval_inst, NexusVM},
memory::Memory,
rv32::{parse::parse_inst, Inst, InstructionSet as RV32IS, LOP, RV32, SOP},
};
use nexus_vm::{instructions::Width, memory::Memory};

use jolt_common::rv_trace as jolt_rv;

use crate::{convert, Error, LOG_TARGET, VM};

/// Trace VM execution in Jolt format.
pub fn trace(vm: VM) -> Result<Vec<jolt_rv::RVTraceRow>, Error> {
pub fn trace<M: Memory>(vm: VM<M>) -> Result<Vec<jolt_rv::RVTraceRow>, Error> {
let VM { mut vm, insts, .. } = vm;

let mut trace = Vec::new();
loop {
// decode next inst
let slice = vm.mem.load(Width::W, vm.regs.pc)?.0.to_le_bytes();
let slice = vm.mem.load(LOP::LW, vm.regs.pc)?.0.to_le_bytes();
let next_inst = parse_inst(vm.regs.pc, &slice)?;
if next_inst.inst == RV32::UNIMP {
break;
}

// save store address for memory state
let store_addr: Option<(Width, u32)> =
let store_addr: Option<(LOP, u32)> =
if let RV32::STORE { rs1, imm, sop, .. } = next_inst.inst {
use nexus_riscv::rv32::SOP::*;
let width = match sop {
SB => Width::BU,
SH => Width::HU,
SW => Width::W,
let lop = match sop {
SOP::SB => LOP::LB,
SOP::SH => LOP::LH,
SOP::SW => LOP::LW,
};

let x = vm.get_reg(rs1);
let addr = add32(x, imm);
Some((width, addr))
Some((lop, addr))
} else {
None
};
@@ -42,6 +41,16 @@ pub fn trace(vm: VM) -> Result<Vec<jolt_rv::RVTraceRow>, Error> {
let mut rv_row = init_trace_row(&vm, next_inst, &inst);

eval_inst(&mut vm)?;

if vm.instruction_sets.contains(&RV32IS::RV32Nexus) {
tracing::debug!(
target: LOG_TARGET,
?inst,
"Unsupported instruction",
);
return Err(Error::Unsupported(next_inst.inst));
}

update_row_post_eval(&vm, &mut rv_row, store_addr);

trace.push(rv_row);
@@ -57,8 +66,8 @@ pub fn trace(vm: VM) -> Result<Vec<jolt_rv::RVTraceRow>, Error> {
Ok(trace)
}

fn init_trace_row(
vm: &NexusVM,
fn init_trace_row<M: Memory>(
vm: &NexusVM<M>,
inst: Inst,
elf_inst: &jolt_rv::ELFInstruction,
) -> jolt_rv::RVTraceRow {
@@ -73,16 +82,16 @@ fn init_trace_row(
}
}

fn update_row_post_eval(
vm: &NexusVM,
fn update_row_post_eval<M: Memory>(
vm: &NexusVM<M>,
rv_trace_row: &mut jolt_rv::RVTraceRow,
store_addr: Option<(Width, u32)>,
store_addr: Option<(LOP, u32)>,
) {
if let Some(rd) = rv_trace_row.instruction.rd {
rv_trace_row.register_state.rd_post_val = Some(vm.get_reg(rd as u32) as u64);
}
if let Some((width, store_addr)) = store_addr {
let new_value = vm.mem.load(width, store_addr).expect("invalid store").0 as u64;
if let Some((lop, store_addr)) = store_addr {
let new_value = vm.mem.load(lop, store_addr).expect("invalid store").0 as u64;
let Some(jolt_rv::MemoryState::Write { post_value, .. }) = &mut rv_trace_row.memory_state
else {
panic!("invalid memory state for store instruction");
@@ -91,18 +100,12 @@ fn update_row_post_eval(
}
}

fn memory_state(vm: &NexusVM, inst: Inst) -> Option<jolt_rv::MemoryState> {
fn memory_state<M: Memory>(vm: &NexusVM<M>, inst: Inst) -> Option<jolt_rv::MemoryState> {
match inst.inst {
RV32::LOAD { rs1, imm, lop, .. } => {
use nexus_riscv::rv32::LOP::*;
let width = match lop {
LB | LBU => Width::BU,
LH | LHU => Width::HU,
LW => Width::W,
};
let x = vm.get_reg(rs1);
let addr = add32(x, imm);
let value = vm.mem.load(width, addr).expect("invalid load").0 as u64;
let value = vm.mem.load(lop, addr).expect("invalid load").0 as u64;

Some(jolt_rv::MemoryState::Read { address: addr as u64, value })
}
1 change: 0 additions & 1 deletion network/Cargo.toml
Original file line number Diff line number Diff line change
@@ -35,7 +35,6 @@ ark-ec.workspace = true
ark-serialize.workspace = true

nexus-config = { path = "../config" }
nexus-riscv = { path = "../riscv" }
nexus-vm = { path = "../vm" }
nexus-prover = { path = "../prover" }
hex = "0.4.3"
5 changes: 2 additions & 3 deletions network/src/bin/pcdnode/post.rs
Original file line number Diff line number Diff line change
@@ -18,8 +18,7 @@ use crate::{
api::NexusAPI::{Error, NexusProof, Program, Query},
request_work, WorkerState, LOG_TARGET,
};
use nexus_riscv::nvm::translate_elf_bytes;
use nexus_vm::{eval::NexusVM, memory::trie::MerkleTrie, trace::trace};
use nexus_vm::{eval::NexusVM, memory::trie::MerkleTrie, parse_elf, trace::trace};

pub fn manage_proof(
mut state: WorkerState,
@@ -105,7 +104,7 @@ fn api(mut state: WorkerState, msg: NexusAPI) -> Result<NexusAPI> {
target: LOG_TARGET,
"received prove-request",
);
let vm = translate_elf_bytes(&elf)?;
let vm = parse_elf::<MerkleTrie>(&elf)?;
let hash = hex::encode(Sha256::digest(&elf));
manage_proof(state, hash.clone(), vm)?;
Ok(NexusProof(Proof { hash, ..Proof::default() }))
1 change: 0 additions & 1 deletion prover/Cargo.toml
Original file line number Diff line number Diff line change
@@ -19,7 +19,6 @@ anyhow = "1.0"

zstd = { version = "0.12", default-features = false }

nexus-riscv = { path = "../riscv" }
nexus-vm = { path = "../vm" }
nexus-nova = { path = "../nova", features = ["spartan"] }
nexus-tui = { path = "../tools/tui" }
2 changes: 1 addition & 1 deletion prover/examples/prove.rs
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ use nexus_config::{
vm::{NovaImpl, ProverImpl},
VmConfig,
};
use nexus_riscv::VMOpts;
use nexus_vm::VMOpts;

use tracing::level_filters::LevelFilter;
use tracing_subscriber::EnvFilter;
4 changes: 2 additions & 2 deletions prover/src/circuit.rs
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ pub use ark_relations::{

use nexus_vm::{
circuit::{build_constraints, ARITY},
eval::halt_vm,
machines::nop_vm,
memory::{path::Path, trie::MerkleTrie},
trace::{trace, Trace},
};
@@ -39,7 +39,7 @@ impl Tr {
}

pub fn nop_circuit(k: usize) -> Result<Tr, ProofError> {
let mut vm = halt_vm::<MerkleTrie>();
let mut vm = nop_vm::<MerkleTrie>(1);
let trace = trace(&mut vm, k, false)?;
Ok(Tr(trace))
}
14 changes: 1 addition & 13 deletions prover/src/error.rs
Original file line number Diff line number Diff line change
@@ -5,16 +5,12 @@ pub use ark_relations::r1cs::SynthesisError;
pub use ark_serialize::SerializationError;
pub use nexus_nova::nova::{pcd::compression::SpartanError, Error as NovaError};
pub use nexus_nova::r1cs::Error as R1CSError;
pub use nexus_riscv::error::VMError as RVError;
pub use nexus_vm::error::NexusVMError;

/// Errors related to proof generation
#[derive(Debug)]
pub enum ProofError {
/// A error occured loading program
VMError(RVError),

/// An error occured executing program
/// An error occured loading or executing program
NexusVMError(NexusVMError),

/// An error occurred reading file system
@@ -52,12 +48,6 @@ pub enum ProofError {
}
use ProofError::*;

impl From<RVError> for ProofError {
fn from(x: RVError) -> ProofError {
VMError(x)
}
}

impl From<NexusVMError> for ProofError {
fn from(x: NexusVMError) -> ProofError {
NexusVMError(x)
@@ -101,7 +91,6 @@ impl From<SpartanError> for ProofError {
impl Error for ProofError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
VMError(e) => Some(e),
NexusVMError(e) => Some(e),
IOError(e) => Some(e),
CircuitError(e) => Some(e),
@@ -121,7 +110,6 @@ impl Error for ProofError {
impl Display for ProofError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
VMError(e) => write!(f, "{e}"),
NexusVMError(e) => write!(f, "{e}"),
IOError(e) => write!(f, "{e}"),
CircuitError(e) => write!(f, "{e}"),
7 changes: 2 additions & 5 deletions prover/src/lib.rs
Original file line number Diff line number Diff line change
@@ -10,8 +10,7 @@ use std::path::Path;

use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};

use nexus_riscv::VMOpts;
use nexus_vm::memory::trie::MerkleTrie;
use nexus_vm::{memory::trie::MerkleTrie, VMOpts};

use nexus_nova::nova::pcd::compression::SNARK;

@@ -68,9 +67,7 @@ pub fn load_proof<P: CanonicalDeserialize>(path: &Path) -> Result<P, ProofError>
type Trace = nexus_vm::trace::Trace<nexus_vm::memory::path::Path>;

pub fn run(opts: &VMOpts, pow: bool) -> Result<Trace, ProofError> {
Ok(nexus_riscv::nvm::run_as_nvm::<MerkleTrie>(
opts, pow, false,
)?)
Ok(nexus_vm::trace_vm::<MerkleTrie>(opts, pow, false)?)
}

pub fn prove_seq(pp: &SeqPP, trace: Trace) -> Result<IVCProof, ProofError> {
2 changes: 1 addition & 1 deletion prover/src/main.rs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ use nexus_prover::{
types::{ComPCDNode, ComPP},
LOG_TARGET,
};
use nexus_riscv::VMOpts;
use nexus_vm::VMOpts;

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
18 changes: 0 additions & 18 deletions riscv/Cargo.toml

This file was deleted.

44 changes: 0 additions & 44 deletions riscv/src/error.rs

This file was deleted.

192 changes: 0 additions & 192 deletions riscv/src/eval.rs

This file was deleted.

152 changes: 0 additions & 152 deletions riscv/src/lib.rs

This file was deleted.

404 changes: 0 additions & 404 deletions riscv/src/nvm.rs

This file was deleted.

2 changes: 1 addition & 1 deletion tools/Cargo.toml
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ clap.workspace = true

nexus-tools-dev = { path = "./tools-dev", default-features = false }
nexus-config = { path = "../config" }
nexus-riscv = { path = "../riscv" }
nexus-vm = { path = "../vm" }
nexus-prover = { path = "../prover", features = ["verbose"] }
nexus-jolt = { path = "../jolt" }
nexus-tui = { path = "./tui" }
5 changes: 3 additions & 2 deletions tools/src/command/jolt.rs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ use std::{fs::File, io::BufReader, path::Path};

use nexus_jolt::{parse, preprocess, trace, JoltCommitments, JoltProof};
use nexus_tools_dev::command::common::prove::CommonProveArgs;
use nexus_vm::memory::trie::MerkleTrie;

use anyhow::Context;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
@@ -14,7 +15,7 @@ type Proof = (JoltProof, JoltCommitments);

pub fn prove(path: &Path) -> anyhow::Result<()> {
let bytes = std::fs::read(path)?;
let vm = parse::parse_elf(&bytes)?;
let vm: nexus_jolt::VM<MerkleTrie> = parse::parse_elf(&bytes)?;

let mut term = nexus_tui::TerminalHandle::new_enabled();

@@ -82,7 +83,7 @@ pub fn verify(proof_path: &Path, prove_args: CommonProveArgs) -> anyhow::Result<
.context("proof is not in Jolt format")?;

let bytes = std::fs::read(path)?;
let vm = parse::parse_elf(&bytes)?;
let vm: nexus_jolt::VM<MerkleTrie> = parse::parse_elf(&bytes)?;

let mut term = nexus_tui::TerminalHandle::new_enabled();

2 changes: 1 addition & 1 deletion tools/src/command/prove.rs
Original file line number Diff line number Diff line change
@@ -114,7 +114,7 @@ fn local_prove(
};
let path_str = pp_file.to_str().context("path is not valid utf8")?;

let opts = nexus_riscv::VMOpts {
let opts = nexus_vm::VMOpts {
k,
machine: None,
file: Some(path.into()),
4 changes: 2 additions & 2 deletions tools/src/command/run.rs
Original file line number Diff line number Diff line change
@@ -21,11 +21,11 @@ fn run_vm(bin: Option<String>, verbose: bool, profile: &str) -> anyhow::Result<(
}

pub fn run_vm_with_elf_file(path: &Path, verbose: bool) -> anyhow::Result<()> {
let opts = nexus_riscv::VMOpts {
let opts = nexus_vm::VMOpts {
k: 1,
machine: None,
file: Some(path.into()),
};

nexus_riscv::run_vm(&opts, verbose).map_err(Into::into)
nexus_vm::run_vm::<nexus_vm::memory::paged::Paged>(&opts, verbose).map_err(Into::into)
}
2 changes: 1 addition & 1 deletion tools/tools-dev/src/command/dev/common_impl/run.rs
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ pub fn run_vm_with_elf_file(path: &Path, verbose: bool) -> anyhow::Result<()> {
vm_opts.push("--trace".into());
}

let mut cargo_opts: Vec<OsString> = ["run", "-p", "nexus-riscv"]
let mut cargo_opts: Vec<OsString> = ["run", "-p", "nexus-vm"]
.into_iter()
.map(From::from)
.collect();
6 changes: 3 additions & 3 deletions vm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -10,10 +10,10 @@ keywords = { workspace = true }
categories = { workspace = true }

[dependencies]
thiserror = "1.0"
clap.workspace = true
elf.workspace = true
serde.workspace = true

thiserror = "1.0"
num-traits = "0.2"
num-derive = "0.4"

@@ -23,4 +23,4 @@ ark-std.workspace = true
ark-relations.workspace = true
ark-serialize.workspace = true
ark-r1cs-std.workspace = true
ark-bn254.workspace = true
ark-bn254.workspace = true
4 changes: 2 additions & 2 deletions vm/src/circuit.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
mod nvm;
mod r1cs;
mod riscv;
mod step;

#[cfg(test)]
mod test;

pub use nvm::ARITY;
pub use r1cs::F;
pub use riscv::ARITY;
pub use step::build_constraints;
1,136 changes: 0 additions & 1,136 deletions vm/src/circuit/nvm.rs

This file was deleted.

123 changes: 32 additions & 91 deletions vm/src/circuit/r1cs.rs
Original file line number Diff line number Diff line change
@@ -7,13 +7,6 @@
//! These matrices are meant to be used at compile-time as a
//! source for generating constraints over a target field.
#![allow(clippy::wrong_self_convention)]

// Historical note: this structure was originally used as
// an intermediate structure before translating to either
// bellman or arkworks. In the current code we only support
// arkworks, and perhaps should be rewritten.

use std::collections::HashMap;
use std::ops::Range;

@@ -74,11 +67,13 @@ impl R1CS {
}
}

#[allow(dead_code)]
#[inline]
pub fn input(&self) -> &[F] {
&self.w[self.input_range()]
}

#[allow(dead_code)]
#[inline]
pub fn output(&self) -> &[F] {
&self.w[self.output_range()]
@@ -113,6 +108,7 @@ impl R1CS {
if self.vars.contains_key(name) {
panic!("local variable override {name}");
}

let n = self.new_var(name);
self.locals.push(name.to_string());
n
@@ -155,18 +151,6 @@ impl R1CS {
self.c.push(c);
}

pub fn equal_scalar(&mut self, name: &str, x: F) {
self.constraint(|cs, a, b, c| {
a[cs.var(name)] = ONE;
b[0] = ONE;
c[0] = x;
});
}

pub fn equal(&mut self, name: &str, val: u32) {
self.equal_scalar(name, F::from(val))
}

pub fn set_eq(&mut self, name: &str, var: &str) -> usize {
let vj = self.var(var);
let j = self.new_var(name);
@@ -190,24 +174,8 @@ impl R1CS {
j
}

pub fn not(&mut self, output: &str, input: &str) -> usize {
let i = self.var(output);
let j = self.var(input);
self.constraint(|_cs, a, b, c| {
a[0] = ONE;
a[j] = MINUS;
b[0] = ONE;
c[i] = ONE;
});
i
}

pub fn set_not(&mut self, output: &str, input: &str) -> usize {
self.set_field_var(output, ONE - self.get_var(input));
self.not(output, input)
}

pub fn to_bits(&mut self, name: &str, val: u32) -> usize {
#[allow(clippy::wrong_self_convention)]
pub fn to_bits(&mut self, name: &str, val: u32) {
mx00s marked this conversation as resolved.
Show resolved Hide resolved
let vj = self.set_var(name, val);
let js: Vec<usize> = (0..32)
.map(|i| self.set_bit(&format!("{name}_{i}"), ((val >> i) & 1) == 1))
@@ -220,19 +188,14 @@ impl R1CS {
b[0] = ONE;
c[vj] = ONE;
});
vj
}

pub fn from_bits(&mut self, output: &str, val: u32, from: &str, start: u32, end: u32) -> usize {
let j = self.set_var(output, val);
pub fn eqi(&mut self, v0: &str, x: F) {
self.constraint(|cs, a, b, c| {
for i in (start..end).rev() {
a[cs.var(&format!("{from}_{i}"))] = F::from(1u64 << (i - start));
}
a[cs.var(v0)] = ONE;
b[0] = ONE;
c[j] = ONE;
c[0] = x;
});
j
}

pub fn add(&mut self, v0: &str, v1: &str, v2: &str) {
@@ -244,11 +207,6 @@ impl R1CS {
});
}

pub fn set_add(&mut self, v0: &str, v1: &str, v2: &str) {
self.set_field_var(v0, self.get_var(v1) + self.get_var(v2));
self.add(v0, v1, v2)
}

pub fn addi(&mut self, v0: &str, v1: &str, x: F) {
self.constraint(|cs, a, b, c| {
a[0] = x;
@@ -266,11 +224,6 @@ impl R1CS {
});
}

pub fn set_mul(&mut self, v0: &str, v1: &str, v2: &str) {
self.set_field_var(v0, self.get_var(v1) * self.get_var(v2));
self.mul(v0, v1, v2)
}

pub fn muli(&mut self, v0: &str, v1: &str, x: F) {
self.constraint(|cs, a, b, c| {
a[0] = x;
@@ -279,6 +232,7 @@ impl R1CS {
});
}

#[allow(dead_code)]
pub fn merge(&mut self, cs: &Self) {
let left_len = self.w.len();
let len = left_len + cs.w.len();
@@ -300,30 +254,39 @@ impl R1CS {
})
}

// note: is_sat is only used in tests, so performance is not
// too important.
#[allow(dead_code)]
pub fn is_sat(&self) -> bool {
assert!(self.a.len() == self.b.len());
assert!(self.a.len() == self.c.len());
debug_assert!(self.a.len() == self.b.len());
debug_assert!(self.a.len() == self.c.len());

#[cfg(debug_assertions)]
for m in [&self.a, &self.b, &self.c] {
for v in m {
assert!(v.len() == self.w.len());
debug_assert!(v.len() == self.w.len());
}
}

#[rustfmt::skip]
fn dot(a: &V, b: &V) -> F {
a.iter().zip(b).map(|(a, b)| a * b).sum()
a.iter()
.zip(b)
.map(|(a,b)| a * b)
.sum()
}

fn multiply_vec(m: &M, v: &V) -> Vec<F> {
m.iter().map(|r| dot(r, v)).collect()
#[rustfmt::skip]
fn MxV(m: &M, v: &V) -> Vec<F> {
m.iter()
.map(|r| dot(r,v))
.collect()
}

let x = multiply_vec(&self.a, &self.w);
let y = multiply_vec(&self.b, &self.w);
let z = multiply_vec(&self.c, &self.w);
let x = MxV(&self.a, &self.w);
let y = MxV(&self.b, &self.w);
let z = MxV(&self.c, &self.w);

#[cfg(debug_assertions)]
#[allow(clippy::needless_range_loop)]
for i in 0..x.len() {
if x[i] * y[i] != z[i] {
println!("constraint {i} not satisfied");
@@ -418,9 +381,10 @@ pub fn member(cs: &mut R1CS, name: &str, k: u32, set: &[u32]) {

// build constraints: l_n-1 = r_0 = 0
// x(x-s1)...(x-s{n-1}) = 0
cs.equal_scalar("r0", ZERO);
cs.equal_scalar(&format!("l{}", n - 1), ZERO);
cs.eqi("r0", ZERO);
cs.eqi(&format!("l{}", n - 1), ZERO);

#[allow(clippy::needless_range_loop)]
for i in 0..n {
//set x-k variables
let si = ZERO - F::from(set[i]);
@@ -626,29 +590,6 @@ mod test {
}
}

#[test]
fn test_from_bits() {
let mut cs = R1CS::default();
let x = cs.to_bits("x", 0xcccccccc);
let y = cs.from_bits("y", 0xcccccccc, "x", 0, 32);
assert!(cs.is_sat());
assert_eq!(cs.w[x], cs.w[y]);

let mut cs = R1CS::default();
let _ = cs.to_bits("x", 0xcccccccc);
let _ = cs.from_bits("y", 0xcccccccc, "x", 0, 32);
let z = cs.from_bits("z", 0xcc, "x", 8, 16);
assert!(cs.is_sat());
assert_eq!(cs.w[z], F::from(0xcc));

let mut cs = R1CS::default();
let _ = cs.to_bits("x", 0xcccccccc);
let _ = cs.from_bits("y", 0xcccccccc, "x", 0, 32);
let z = cs.from_bits("z", 0x33, "x", 2, 8);
assert!(cs.is_sat());
assert_eq!(cs.w[z], F::from(0x33));
}

fn test_mem(set: &[u32]) {
for &x in set {
let mut cs = R1CS::default();
1,672 changes: 1,672 additions & 0 deletions vm/src/circuit/riscv.rs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion vm/src/circuit/step.rs
Original file line number Diff line number Diff line change
@@ -16,8 +16,8 @@ use crate::{
};

use super::{
nvm::{step, ARITY},
r1cs::{R1CS, V, ZERO},
riscv::{step, ARITY},
F,
};

11 changes: 6 additions & 5 deletions vm/src/circuit/test.rs
Original file line number Diff line number Diff line change
@@ -3,16 +3,17 @@ use ark_relations::r1cs::ConstraintSystem;

use crate::{
error::Result,
eval::{halt_vm, NexusVM},
eval::NexusVM,
machines::loop_vm,
memory::{trie::MerkleTrie, Memory},
trace::trace,
};

use super::{nvm::step, r1cs::R1CS, step::build_constraints, F};
use super::{r1cs::R1CS, riscv::step, step::build_constraints, F};

// generate R1CS matrices
fn vm_circuit(k: usize) -> Result<R1CS> {
let mut vm = halt_vm::<MerkleTrie>();
let mut vm = loop_vm::<MerkleTrie>(5);
let tr = trace(&mut vm, k, false)?;
let w = tr.blocks[0].into_iter().next().unwrap();
Ok(step(&w, false))
@@ -38,7 +39,7 @@ fn nvm_check_steps(mut vm: NexusVM<impl Memory>) -> Result<()> {
#[test]
#[ignore]
fn nvm_step() {
let vm = halt_vm::<MerkleTrie>();
let vm = loop_vm::<MerkleTrie>(5);
nvm_check_steps(vm).unwrap();
}

@@ -61,7 +62,7 @@ fn ark_check(mut vm: NexusVM<impl Memory>, k: usize) -> Result<()> {
}

fn ark_check_steps(k: usize) {
let vm = halt_vm::<MerkleTrie>();
let vm = loop_vm::<MerkleTrie>(5);
ark_check(vm, k).unwrap();
}

41 changes: 29 additions & 12 deletions vm/src/error.rs
Original file line number Diff line number Diff line change
@@ -3,17 +3,37 @@ use thiserror::Error;
/// Errors related to VM initialization and execution
#[derive(Debug, Error)]
pub enum NexusVMError {
/// not enough bytes available to complete instruction parse
#[error("partial instruction at pc:{0:x}")]
PartialInstruction(u32),

/// Invalid instruction size found during parse
#[error("invalid instruction size, {1}, at pc:{0:x}")]
InvalidSize(u32, u32),

/// Invalid instruction format, could not parse
#[error("invalid instruction {1} at {0}")]
InvalidInstruction(u64, u32),
#[error("invalid instruction {1:x} at pc:{0:x}")]
InvalidInstruction(u32, u32),

/// Unknown ECALL number
#[error("unknown syscall {1} at {0}")]
UnknownSyscall(u32, u32),
#[error("unknown ecall {1} at pc:{0:x}")]
UnknownECall(u32, u32),

/// An I/O error occurred
#[error(transparent)]
IOError(#[from] std::io::Error),

/// Invalid memory address
#[error("invalid memory access {0:x}")]
SegFault(u32),
/// Unknown (test) machine
#[error("unknown machine {0}")]
UnknownMachine(String),

/// An error occurred while parsing the ELF headers
#[error(transparent)]
ELFError(#[from] elf::ParseError),

/// ELF format not supported
#[error("ELF format not supported: {0}")]
ELFFormat(&'static str),

/// Invalid memory alignment
#[error("misaligned memory access {0:x}")]
@@ -22,10 +42,7 @@ pub enum NexusVMError {
/// An error occured while hashing
#[error("error hashing {0}")]
HashError(String),

/// An error occurred reading file system
#[error(transparent)]
IOError(#[from] std::io::Error),
}

pub(crate) type Result<T, E = NexusVMError> = std::result::Result<T, E>;
/// Result type for VM functions that can produce errors
pub type Result<T, E = NexusVMError> = std::result::Result<T, E>;
345 changes: 220 additions & 125 deletions vm/src/eval.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
//! Evaluation for Nexus VM programs.
//! A Virtual Machine for RISC-V
use num_traits::FromPrimitive;
use crate::{
error::*,
memory::Memory,
rv32::{parse::*, *},
syscalls::Syscalls,
};

use crate::error::{NexusVMError::InvalidInstruction, Result};
use crate::instructions::{Inst, Opcode, Opcode::*, Width};
use crate::memory::Memory;
use crate::syscalls::Syscalls;
use std::collections::HashSet;

/// State of a running Nexus VM program.
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use serde::{Deserialize, Serialize};

/// virtual machine state
#[derive(Default)]
pub struct NexusVM<M: Memory> {
/// Current program counter.
pub pc: u32,
/// Register file.
pub regs: [u32; 32],
/// Most recent instruction.
pub inst: Inst,
/// Result of most recent instruction.
pub Z: u32,
/// ISA registers
pub regs: Regs,
/// Syscall implementation
pub syscalls: Syscalls,
/// current instruction
pub inst: Inst,
/// internal result register
pub Z: u32,
/// used instruction sets
pub instruction_sets: HashSet<InstructionSet>,
mx00s marked this conversation as resolved.
Show resolved Hide resolved
/// Machine memory.
pub memory: M,
pub mem: M,
/// Memory proof for current instruction at pc
pub pc_proof: M::Proof,
/// Memory proof for load/store instructions.
@@ -30,150 +35,240 @@ pub struct NexusVM<M: Memory> {
pub write_proof: Option<M::Proof>,
}

/// Generate a trivial VM with a single NOP and a single HALT instruction.
pub fn halt_vm<M: Memory>() -> NexusVM<M> {
let mut vm = NexusVM::<M>::default();
let inst = Inst { opcode: NOP, ..Inst::default() };
vm.memory.write_inst(vm.pc, inst.into()).unwrap();
let inst = Inst { opcode: HALT, ..Inst::default() };
vm.memory.write_inst(vm.pc + 8, inst.into()).unwrap();
vm
/// ISA defined registers
#[derive(
Debug,
Clone,
PartialEq,
Default,
Serialize,
Deserialize,
CanonicalSerialize,
CanonicalDeserialize,
)]
pub struct Regs {
/// ISA defined program counter register
pub pc: u32,
/// ISA defined registers x0-x31
pub x: [u32; 32],
}

impl<M: Memory> NexusVM<M> {
pub fn new(pc: u32) -> Self {
let mut vm = Self::default();

vm.regs.pc = pc;
vm.instruction_sets = HashSet::new();

vm
}

/// get value of register r
pub fn get_reg(&self, r: u32) -> u32 {
if r == 0 {
0
} else {
self.regs.x[r as usize]
}
}

/// set value of register r
pub fn set_reg(&mut self, r: u32, val: u32) {
if r != 0 {
self.regs.x[r as usize] = val;
}
}

/// initialize memory from slice
pub fn init_memory(&mut self, addr: u32, bytes: &[u8]) -> Result<()> {
// slow, but simple
for (i, b) in bytes.iter().enumerate() {
self.mem.store(SOP::SB, addr + (i as u32), *b as u32)?;
}
Ok(())
}
}

// A simple, stable peephole optimizer for local constant propagation.
//
// Introduced for old 32-bit -> 64-bit translation, currently unused.
#[allow(dead_code)]
fn peephole(insn: &mut [Inst]) {
for i in 0..insn.len() {
match const_prop(&insn[i..]) {
None => (),
Some(v) => {
for (j, x) in v.iter().enumerate() {
insn[i + j] = *x;
}
}
}
}
}

#[inline]
fn add32(x: u32, y: u32) -> u32 {
x.overflowing_add(y).0
#[allow(dead_code)]
fn const_prop(insn: &[Inst]) -> Option<Vec<Inst>> {
match insn {
[Inst {
pc: pc1,
inst: RV32::AUIPC { rd: rd1, imm: imm1 },
..
}, Inst {
pc: pc2,
inst: RV32::JALR { rd: rd2, rs1, imm: imm2 },
..
}, ..]
if rd1 == rs1 =>
{
let target = add32(add32(*pc1, *imm1), *imm2);
Some(vec![
Inst {
pc: *pc1,
len: 4,
word: 0,
inst: RV32::ALUI { aop: AOP::ADD, rd: 0, rs1: 0, imm: 0 },
},
Inst {
pc: *pc2,
len: 4,
word: 0,
inst: RV32::JALR { rd: *rd2, rs1: 0, imm: target },
},
])
}
_ => None,
}
}

#[inline]
fn mul32(x: u32, y: u32) -> u32 {
x.overflowing_mul(y).0
pub fn add32(a: u32, b: u32) -> u32 {
a.overflowing_add(b).0
}

#[inline]
fn sub32(x: u32, y: u32) -> u32 {
x.overflowing_sub(y).0
pub fn sub32(a: u32, b: u32) -> u32 {
a.overflowing_sub(b).0
}

// Evaluator for branch conditions.
fn brcc(opcode: Opcode, x: u32, y: u32) -> bool {
match opcode {
fn br_op(bop: BOP, x: u32, y: u32) -> bool {
match bop {
BEQ => x == y,
BNE => x != y,
BLT => (x as i32) < (y as i32),
BGE => (x as i32) >= (y as i32),
BLTU => x < y,
BGEU => x >= y,
_ => unreachable!(),
}
}

/// Execute one step of a running Nexus VM.
/// This function will load the next instruction at the address
/// located at the program counter, execute the instruction,
/// and update the register file, program counter, and merkle
/// proofs.
pub fn eval_step(vm: &mut NexusVM<impl Memory>) -> Result<()> {
let (dword, proof) = vm.memory.read_inst(vm.pc)?;
let Some(inst) = Inst::from_u64(dword) else {
return Err(InvalidInstruction(dword, vm.pc));
};

let I = inst.imm;
let X = vm.regs[inst.rs1 as usize];
let Y = vm.regs[inst.rs2 as usize];

let YI = add32(Y, I);
let shamt = YI & 0x1f;
fn alu_op(aop: AOP, x: u32, y: u32) -> u32 {
let shamt = y & 0x1f;
match aop {
ADD => add32(x, y),
SUB => sub32(x, y),
SLT => ((x as i32) < (y as i32)) as u32,
SLTU => (x < y) as u32,
SLL => x << shamt,
SRL => x >> shamt,
SRA => ((x as i32) >> shamt) as u32,
AND => x & y,
OR => x | y,
XOR => x ^ y,
}
}

let mut PC = 0u32;
/// evaluate next instruction
pub fn eval_inst(vm: &mut NexusVM<impl Memory>) -> Result<()> {
let (word, proof) = vm.mem.read_inst(vm.regs.pc)?;
vm.inst = parse_inst(vm.regs.pc, &word.to_le_bytes())?;

vm.inst = inst;
// initialize micro-architecture state
vm.Z = 0;
let mut RD = 0u32;
let mut PC = 0;

vm.pc_proof = proof;
vm.read_proof = None;
vm.write_proof = None;

match inst.opcode {
NOP => {}
HALT => {
PC = vm.pc;
}
SYS => {
vm.Z = vm.syscalls.syscall(vm.pc, vm.regs, &vm.memory)?;
}
JAL => {
vm.Z = add32(vm.pc, 8);
let XI = add32(X, I);
// This semantics treats canonical call/ret
// differently from general jalr.
// TODO: seperate call/ret into their own instructions.
if inst.rs1 <= 1 {
PC = XI;
} else {
PC = mul32(XI, 2);
}
vm.instruction_sets.insert(vm.inst.inst.instruction_set());

match vm.inst.inst {
LUI { rd, imm } => {
RD = rd;
vm.Z = imm;
}
BEQ | BNE | BLT | BGE | BLTU | BGEU => {
if brcc(inst.opcode, X, Y) {
PC = add32(vm.pc, I);
AUIPC { rd, imm } => {
RD = rd;
vm.Z = add32(vm.regs.pc, imm);
}
JAL { rd, imm } => {
RD = rd;
vm.Z = add32(vm.regs.pc, 4);
PC = add32(vm.regs.pc, imm);
}
JALR { rd, rs1, imm } => {
let X = vm.get_reg(rs1);
RD = rd;
vm.Z = add32(vm.regs.pc, 4);
PC = add32(X, imm);
}
BR { bop, rs1, rs2, imm } => {
let X = vm.get_reg(rs1);
let Y = vm.get_reg(rs2);

if br_op(bop, X, Y) {
PC = add32(vm.regs.pc, imm);
}
}
LOAD { lop, rd, rs1, imm } => {
let X = vm.get_reg(rs1);
RD = rd;

LB | LH | LW | LBU | LHU => {
// Note: unwrap cannot fail
let width = Width::try_from(inst.opcode).unwrap();
let addr = add32(X, I);
let (val, proof) = vm.memory.load(width, addr)?;
let addr = add32(X, imm);
let (val, proof) = vm.mem.load(lop, addr)?;
vm.read_proof = Some(proof);
vm.Z = val;
}
SB | SH | SW => {
// Note: unwrap cannot fail
let width = Width::try_from(inst.opcode).unwrap();
let addr = add32(X, I);
let (_, proof) = vm.memory.load(width, addr)?;
vm.read_proof = Some(proof);
vm.write_proof = Some(vm.memory.store(width, addr, Y)?);
}

ADD => vm.Z = add32(X, YI),
SUB => vm.Z = sub32(X, YI),
SLT => vm.Z = ((X as i32) < (YI as i32)) as u32,
SLTU => vm.Z = (X < YI) as u32,
SLL => vm.Z = X << shamt,
SRL => vm.Z = X >> shamt,
SRA => vm.Z = ((X as i32) >> shamt) as u32,
AND => vm.Z = X & YI,
OR => vm.Z = X | YI,
XOR => vm.Z = X ^ YI,
}
STORE { sop, rs1, rs2, imm } => {
let X = vm.get_reg(rs1);
let Y = vm.get_reg(rs2);

if inst.rd > 0 {
vm.regs[inst.rd as usize] = vm.Z;
}
let addr = add32(X, imm);
let lop = match sop {
SB => LB,
SH => LH,
SW => LW,
};

if PC == 0 {
vm.pc = add32(vm.pc, 8);
} else {
vm.pc = PC;
}

Ok(())
}

/// Run a VM to completion. The VM will stop when it encounters
/// a HALT instruction.
pub fn eval(vm: &mut NexusVM<impl Memory>, verbose: bool) -> Result<()> {
loop {
let pc = vm.pc;
eval_step(vm)?;
if verbose {
println!("{:x} {:?}", pc, vm.inst);
let (_, proof) = vm.mem.load(lop, addr)?;
vm.read_proof = Some(proof);
vm.write_proof = Some(vm.mem.store(sop, addr, Y)?);
}
if vm.inst.opcode == HALT {
break;
ALUI { aop, rd, rs1, imm } => {
RD = rd;
let X = vm.get_reg(rs1);
vm.Z = alu_op(aop, X, imm);
}
ALU { aop, rd, rs1, rs2 } => {
let X = vm.get_reg(rs1);
let Y = vm.get_reg(rs2);
RD = rd;
vm.Z = alu_op(aop, X, Y);
}
FENCE => {}
EBREAK { .. } => {}
ECALL { rd } => {
RD = rd;
vm.Z = vm.syscalls.syscall(vm.regs.pc, vm.regs.x, &vm.mem)?;
}
UNIMP => {
PC = vm.inst.pc;
}
}

if PC == 0 {
PC = add32(vm.inst.pc, vm.inst.len);
}
vm.set_reg(RD, vm.Z);
vm.regs.pc = PC;
Ok(())
}
205 changes: 0 additions & 205 deletions vm/src/instructions.rs

This file was deleted.

183 changes: 176 additions & 7 deletions vm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,188 @@
#![allow(dead_code)]
//! A RISC-V virtual machine designed for verified computing
#![allow(non_snake_case)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::field_reassign_with_default)]

// We rely on this in cacheline.rs
#[cfg(not(target_endian = "little"))]
compile_error!("Host must be little-endian");

pub mod error;
pub mod eval;
pub mod instructions;
pub mod machines;
pub mod rv32;

pub mod syscalls;
pub mod trace;

mod ark_serde;
pub mod memory;

pub mod circuit;

use clap::Args;
use elf::{abi::PT_LOAD, endian::LittleEndian, ElfBytes};
use std::fs::read;
use std::path::PathBuf;
use std::time::Instant;

pub use error::*;
use eval::*;
use memory::*;
use rv32::*;
use trace::*;

// don't break API
pub use machines::{loop_vm, nop_vm};

// re-export
#[doc(hidden)]
pub use elf;

/// Load a VM state from an ELF file
pub fn load_elf<M: Memory>(path: &PathBuf) -> Result<NexusVM<M>> {
let file_data = read(path)?;
let slice = file_data.as_slice();
parse_elf(slice)
}

#[doc(hidden)]
pub fn parse_elf_bytes(bytes: &[u8]) -> Result<ElfBytes<LittleEndian>> {
let file = ElfBytes::<LittleEndian>::minimal_parse(bytes)?;
Ok(file)
}

#[doc(hidden)]
pub fn init_vm<M: Memory>(elf: &ElfBytes<LittleEndian>, data: &[u8]) -> Result<NexusVM<M>> {
let e_entry = elf.ehdr.e_entry as u32;

let load_phdrs = elf
.segments()
.unwrap()
.iter()
.filter(|phdr| phdr.p_type == PT_LOAD);

let mut vm = NexusVM::new(e_entry);
for p in load_phdrs {
let s = p.p_offset as usize;
let e = (p.p_offset + p.p_filesz) as usize;
let bytes = &data[s..e];
vm.init_memory(p.p_vaddr as u32, bytes)?;
}
Ok(vm)
}

pub fn parse_elf<M: Memory>(bytes: &[u8]) -> Result<NexusVM<M>> {
let file = parse_elf_bytes(bytes)?;
init_vm(&file, bytes)
}

/// A structure describing a VM to load.
/// This structure can be used with clap.
#[derive(Default, Debug, Args)]
pub struct VMOpts {
/// Instructions per step
#[arg(short, name = "k", default_value = "1")]
pub k: usize,

/// Use a named test machine
#[arg(group = "vm", long, long_help(list_machines()))]
pub machine: Option<String>,

/// Input file, RISC-V 32i ELF
#[arg(group = "vm", required = true)]
pub file: Option<std::path::PathBuf>,
}

fn list_machines() -> String {
let ms = machines::MACHINES
.iter()
.map(|m| m.0.to_string())
.collect::<Vec<String>>()
.join(", ");
"Use a named machine: ".to_string() + &ms
}

/// Load the VM described by `opts`
pub fn load_vm<M: Memory>(opts: &VMOpts) -> Result<NexusVM<M>> {
if let Some(m) = &opts.machine {
if let Some(vm) = machines::lookup_test_machine(m) {
Ok(vm)
} else {
Err(NexusVMError::UnknownMachine(m.clone()))
}
} else {
load_elf(opts.file.as_ref().unwrap())
}
}

/// Evaluate a program starting from a given machine state
pub fn eval(vm: &mut NexusVM<impl Memory>, show: bool) -> Result<()> {
if show {
println!("\nExecution:");
println!(
"{:7} {:8} {:32} {:>8} {:>8}",
"pc", "mem[pc]", "inst", "Z", "PC"
);
}
let t = std::time::Instant::now();
let mut count = 0;

loop {
eval_inst(vm)?;
count += 1;
if show {
println!("{:50} {:8x} {:8x}", vm.inst, vm.Z, vm.regs.pc);
}
if vm.inst.inst == RV32::UNIMP {
break;
}
}

fn table(name: &str, mem: &[u32]) {
for (i, w) in mem.iter().enumerate() {
print!(" {}{:02}: {:8x}", name, i, w);
if (i % 8) == 7 {
println!();
}
}
println!();
}

if show {
println!("\nFinal Machine State: pc: {:x}", vm.regs.pc);
table("x", &vm.regs.x);

println!("Executed {count} instructions in {:?}", t.elapsed());
}
Ok(())
}

/// Load and run an ELF file
pub fn run_vm<M: Memory>(vm: &VMOpts, show: bool) -> Result<()> {
let mut vm: NexusVM<M> = load_vm(vm)?;
eval(&mut vm, show)
}

/// Load and run an ELF file, then return the execution trace
pub fn trace_vm<M: Memory>(
opts: &VMOpts,
pow: bool,
show: bool,
) -> Result<Trace<M::Proof>, NexusVMError> {
let mut vm = load_vm::<M>(opts)?;

if show {
println!("Executing program...");
}

let start = Instant::now();
let trace = trace::<M>(&mut vm, opts.k, pow)?;

if show {
println!(
"Executed {} instructions in {:?}. {} bytes used by trace.",
trace.k * trace.blocks.len(),
start.elapsed(),
&trace.estimate_size(),
);
}

Ok(trace)
}
23 changes: 12 additions & 11 deletions riscv/src/machines.rs → vm/src/machines.rs
Original file line number Diff line number Diff line change
@@ -3,8 +3,8 @@
#![allow(clippy::field_reassign_with_default)]
#![allow(clippy::identity_op)]

use crate::{Regs, VM};
use nexus_vm::{instructions::Width, memory::Memory};
use super::{memory::Memory, rv32::SOP};
use crate::{NexusVM, Regs};

/// An array of test machines, useful for debugging and developemnt.
#[allow(clippy::type_complexity)]
@@ -21,7 +21,7 @@ pub const MACHINES: &[(&str, fn() -> Vec<u32>, fn() -> Regs)] = &[
];

/// Lookup and initialize a test VM by name
pub fn lookup_test_machine(name: &str) -> Option<VM> {
pub fn lookup_test_machine<M: Memory>(name: &str) -> Option<NexusVM<M>> {
Some(assemble(&lookup_test_code(name)?))
}

@@ -35,16 +35,16 @@ pub fn lookup_test_code(name: &str) -> Option<Vec<u32>> {
None
}

fn assemble(words: &[u32]) -> VM {
let mut vm = VM::new(0);
fn assemble<M: Memory>(words: &[u32]) -> NexusVM<M> {
let mut vm = NexusVM::<M>::new(0);
for (i, w) in words.iter().enumerate() {
vm.mem.store(Width::W, i as u32 * 4, *w).unwrap();
vm.mem.store(SOP::SW, i as u32 * 4, *w).unwrap();
}
vm
}

/// Create a VM with k no-op instructions
pub fn nop_vm(k: usize) -> VM {
pub fn nop_vm<M: Memory>(k: usize) -> NexusVM<M> {
assemble(&nop_code(k))
}

@@ -60,7 +60,7 @@ fn nop_result(k: usize) -> Regs {
}

/// Create a VM which loops k times
pub fn loop_vm(k: usize) -> VM {
pub fn loop_vm<M: Memory>(k: usize) -> NexusVM<M> {
assemble(&loop_code(k))
}

@@ -255,7 +255,7 @@ fn ldst_code() -> Vec<u32> {
0xffc02103, // lw x2,-4(x0)
0x00100183, // lb x3,1(x0)
0x00104203, // lbu x4,1(x0)
0xc0001073, // unimp
0xc0001073, // unimp
]
}

@@ -292,7 +292,7 @@ fn shift_code() -> Vec<u32> {
0x4010d793, // srai x15,x1,0x1
0x40a0d813, // srai x16,x1,0xa
0x41f0d893, // srai x17,x1,0x1f
0xc0001073, // unimp
0xc0001073, // unimp
]
}

@@ -375,12 +375,13 @@ fn sub_result() -> Regs {
mod test {
use super::*;
use crate::eval;
use crate::trie::MerkleTrie;

#[test]
fn test_machines() {
for (name, f_code, f_result) in MACHINES {
println!("Testing machine {name}");
let mut vm = assemble(&f_code());
let mut vm: NexusVM<MerkleTrie> = assemble(&f_code());
eval(&mut vm, false).unwrap();
let regs = f_result();
assert_eq!(regs, vm.regs);
4 changes: 2 additions & 2 deletions riscv/src/main.rs → vm/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use nexus_riscv::*;
use nexus_vm::{memory::trie::MerkleTrie, run_vm, VMOpts};

use clap::Parser;

@@ -15,7 +15,7 @@ pub struct Opts {

fn main() {
let opts = Opts::parse();
match run_vm(&opts.vm, opts.trace) {
match run_vm::<MerkleTrie>(&opts.vm, opts.trace) {
Ok(()) => (),
Err(e) => println!("{e}"),
}
100 changes: 50 additions & 50 deletions vm/src/memory.rs
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};

use crate::circuit::F;
use crate::error::Result;
use crate::instructions::Width;
use crate::rv32::{LOP, SOP};
use cacheline::CacheLine;

/// A `MemoryProof` implementation provides the memory commitments and
@@ -59,33 +59,33 @@ pub trait Memory: Default {
F: Fn(&mut CacheLine) -> Result<()>;

/// read instruction at address
fn read_inst(&self, addr: u32) -> Result<(u64, Self::Proof)> {
fn read_inst(&self, addr: u32) -> Result<(u32, Self::Proof)> {
let (cl, path) = self.query(addr);
Ok((cl.ldw(addr)?, path))
Ok((cl.lw(addr)?, path))
}

/// write instruction at address
fn write_inst(&mut self, addr: u32, val: u64) -> Result<()> {
let _ = self.update(addr, |cl| cl.sdw(addr, val))?;
fn write_inst(&mut self, addr: u32, val: u32) -> Result<()> {
let _ = self.update(addr, |cl| cl.sw(addr, val))?;
Ok(())
}

/// perform load according to `width`
fn load(&self, width: Width, addr: u32) -> Result<(u32, Self::Proof)> {
/// perform load according to `lop`
fn load(&self, lop: LOP, addr: u32) -> Result<(u32, Self::Proof)> {
let (cl, path) = self.query(addr);
Ok((cl.load(width, addr)?, path))
Ok((cl.load(lop, addr)?, path))
}

/// perform store according to `width`
fn store(&mut self, width: Width, addr: u32, val: u32) -> Result<Self::Proof> {
self.update(addr, |cl| cl.store(width, addr, val))
/// perform store according to `sop`
fn store(&mut self, sop: SOP, addr: u32, val: u32) -> Result<Self::Proof> {
self.update(addr, |cl| cl.store(sop, addr, val))
}
}

#[cfg(test)]
mod test {
use super::{paged::Paged, trie::MerkleTrie, *};
use Width::*;
use crate::rv32::{LOP::*, SOP::*};

#[test]
fn test_mem_merkle() {
@@ -99,43 +99,43 @@ mod test {

fn test_mem(mut mem: impl Memory) {
// read before write
assert_eq!(mem.load(W, 0x1000).unwrap().0, 0);

mem.store(W, 0x1000, 1).unwrap();
mem.store(B, 0x1100, 1).unwrap();
mem.store(B, 0x1101, 2).unwrap();
mem.store(B, 0x1103, 3).unwrap();
mem.store(B, 0x1104, 4).unwrap();
mem.store(B, 0x11000, 1).unwrap();

assert_eq!(mem.load(BU, 0x10ff).unwrap().0, 0);
assert_eq!(mem.load(BU, 0x1100).unwrap().0, 1);
assert_eq!(mem.load(BU, 0x1101).unwrap().0, 2);
assert_eq!(mem.load(BU, 0x1103).unwrap().0, 3);
assert_eq!(mem.load(BU, 0x1104).unwrap().0, 4);
assert_eq!(mem.load(BU, 0x1105).unwrap().0, 0);
assert_eq!(mem.load(BU, 0x11000).unwrap().0, 1);
assert_eq!(mem.load(BU, 0x11001).unwrap().0, 0);

mem.store(H, 0x1100, 0x708).unwrap();
assert_eq!(mem.load(BU, 0x1100).unwrap().0, 8);
assert_eq!(mem.load(BU, 0x1101).unwrap().0, 7);
assert_eq!(mem.load(HU, 0x1100).unwrap().0, 0x708);
assert_eq!(mem.load(HU, 0x1200).unwrap().0, 0);

mem.store(W, 0x1200, 0x10203040).unwrap();
assert_eq!(mem.load(BU, 0x1200).unwrap().0, 0x40);
assert_eq!(mem.load(BU, 0x1201).unwrap().0, 0x30);
assert_eq!(mem.load(BU, 0x1202).unwrap().0, 0x20);
assert_eq!(mem.load(BU, 0x1203).unwrap().0, 0x10);
assert_eq!(mem.load(HU, 0x1200).unwrap().0, 0x3040);
assert_eq!(mem.load(HU, 0x1202).unwrap().0, 0x1020);
assert_eq!(mem.load(W, 0x1200).unwrap().0, 0x10203040);

mem.store(H, 0x1300, 0x81).unwrap();
assert_eq!(mem.load(B, 0x1300).unwrap().0, 0xffffff81);

mem.store(H, 0x1300, 0x8321).unwrap();
assert_eq!(mem.load(H, 0x1300).unwrap().0, 0xffff8321);
assert_eq!(mem.load(LW, 0x1000).unwrap().0, 0);

mem.store(SW, 0x1000, 1).unwrap();
mem.store(SB, 0x1100, 1).unwrap();
mem.store(SB, 0x1101, 2).unwrap();
mem.store(SB, 0x1103, 3).unwrap();
mem.store(SB, 0x1104, 4).unwrap();
mem.store(SB, 0x11000, 1).unwrap();

assert_eq!(mem.load(LBU, 0x10ff).unwrap().0, 0);
assert_eq!(mem.load(LBU, 0x1100).unwrap().0, 1);
assert_eq!(mem.load(LBU, 0x1101).unwrap().0, 2);
assert_eq!(mem.load(LBU, 0x1103).unwrap().0, 3);
assert_eq!(mem.load(LBU, 0x1104).unwrap().0, 4);
assert_eq!(mem.load(LBU, 0x1105).unwrap().0, 0);
assert_eq!(mem.load(LBU, 0x11000).unwrap().0, 1);
assert_eq!(mem.load(LBU, 0x11001).unwrap().0, 0);

mem.store(SH, 0x1100, 0x708).unwrap();
assert_eq!(mem.load(LBU, 0x1100).unwrap().0, 8);
assert_eq!(mem.load(LBU, 0x1101).unwrap().0, 7);
assert_eq!(mem.load(LHU, 0x1100).unwrap().0, 0x708);
assert_eq!(mem.load(LHU, 0x1200).unwrap().0, 0);

mem.store(SW, 0x1200, 0x10203040).unwrap();
assert_eq!(mem.load(LBU, 0x1200).unwrap().0, 0x40);
assert_eq!(mem.load(LBU, 0x1201).unwrap().0, 0x30);
assert_eq!(mem.load(LBU, 0x1202).unwrap().0, 0x20);
assert_eq!(mem.load(LBU, 0x1203).unwrap().0, 0x10);
assert_eq!(mem.load(LHU, 0x1200).unwrap().0, 0x3040);
assert_eq!(mem.load(LHU, 0x1202).unwrap().0, 0x1020);
assert_eq!(mem.load(LW, 0x1200).unwrap().0, 0x10203040);

mem.store(SH, 0x1300, 0x81).unwrap();
assert_eq!(mem.load(LB, 0x1300).unwrap().0, 0xffffff81);

mem.store(SH, 0x1300, 0x8321).unwrap();
assert_eq!(mem.load(LH, 0x1300).unwrap().0, 0xffff8321);
}
}
95 changes: 27 additions & 68 deletions vm/src/memory/cacheline.rs
Original file line number Diff line number Diff line change
@@ -6,8 +6,7 @@ use ark_bn254::Fr as F;
use ark_ff::PrimeField;

use crate::error::*;
use crate::instructions::{Width, Width::*};

use crate::rv32::*;
use NexusVMError::Misaligned;

/// A CacheLine represents the smallest unit of memory that can be read
@@ -16,20 +15,16 @@ use NexusVMError::Misaligned;
#[derive(Copy, Clone)]
pub union CacheLine {
pub dwords: [u64; 4],
pub words: [u32; 8],
pub halfs: [u16; 16],
pub bytes: [u8; 32],
pub(crate) words: [u32; 8],
pub(crate) halfs: [u16; 16],
pub(crate) bytes: [u8; 32],
}

/// The number of bits of address the cacheline holds
pub const CACHE_BITS: usize = 5;

/// The number of ignored bits in the address
pub const IGNORED_BITS: usize = 11;

/// The log of the number of `CacheLines` in a complete memory.
pub const CACHE_LOG: usize = 22 - CACHE_BITS;
pub const CACHE_LOG: usize = 32 - CACHE_BITS;

// This will generate a compile error if CacheLine is not the right size
const _: fn() = || {
@@ -62,7 +57,7 @@ impl Debug for CacheLine {

impl PartialEq for CacheLine {
fn eq(&self, other: &CacheLine) -> bool {
unsafe { self.dwords == other.dwords }
unsafe { self.words == other.words }
}
}

@@ -78,28 +73,29 @@ impl CacheLine {

// return slice at address. This slice will only extend to the
// end of the cacheline. (used by instruction parsing)
//pub(crate) fn bytes(&self, addr: u32) -> &[u8] {
// let offset = (addr & 31) as usize;
// unsafe { &self.bytes[offset..] }
//}

/// perform load according to `width`
pub fn load(&self, width: Width, addr: u32) -> Result<u32> {
match width {
B => self.lb(addr),
H => self.lh(addr),
W => self.lw(addr),
BU => self.lbu(addr),
HU => self.lhu(addr),
#[allow(dead_code)]
pub(crate) fn bytes(&self, addr: u32) -> &[u8] {
let offset = (addr & 31) as usize;
unsafe { &self.bytes[offset..] }
}

/// perform load according to `lop`
pub fn load(&self, lop: LOP, addr: u32) -> Result<u32> {
match lop {
LB => self.lb(addr),
LH => self.lh(addr),
LW => self.lw(addr),
LBU => self.lbu(addr),
LHU => self.lhu(addr),
}
}

/// perform store according to `width`
pub fn store(&mut self, width: Width, addr: u32, val: u32) -> Result<()> {
match width {
B | BU => self.sb(addr, val as u8),
H | HU => self.sh(addr, val as u16),
W => self.sw(addr, val),
/// perform store according to `sop`
pub fn store(&mut self, sop: SOP, addr: u32, val: u32) -> Result<()> {
match sop {
SB => self.sb(addr, val as u8),
SH => self.sh(addr, val as u16),
SW => self.sw(addr, val),
}
}

@@ -126,14 +122,6 @@ impl CacheLine {
unsafe { Ok(self.words[((addr >> 2) & 7) as usize]) }
}

/// load 64-bit value at addr
pub fn ldw(&self, addr: u32) -> Result<u64> {
if (addr & 7) != 0 {
return Err(Misaligned(addr));
}
unsafe { Ok(self.dwords[((addr >> 3) & 3) as usize]) }
}

/// load byte at addr, sign-extended
pub fn lb(&self, addr: u32) -> Result<u32> {
let val = self.lbu(addr)?;
@@ -183,17 +171,6 @@ impl CacheLine {
}
Ok(())
}

/// store 64-bit value at addr
pub fn sdw(&mut self, addr: u32, val: u64) -> Result<()> {
if (addr & 7) != 0 {
return Err(Misaligned(addr));
}
unsafe {
self.dwords[((addr >> 3) & 3) as usize] = val;
}
Ok(())
}
}

#[cfg(test)]
@@ -236,17 +213,6 @@ mod test {
assert_eq!(cache.lw(0).unwrap(), 0x0c0d0a0b);
}

#[test]
fn cache_64() {
let mut cache = CacheLine::from([0; 32]);
cache.sw(0, 0x01020304).unwrap();
cache.sw(4, 0x05060708).unwrap();
cache.sw(8, 0x01020304).unwrap();
cache.sw(12, 0x05060708).unwrap();
assert_eq!(cache.ldw(0).unwrap(), 0x0506070801020304);
assert_eq!(cache.ldw(8).unwrap(), 0x0506070801020304);
}

#[test]
#[should_panic]
fn cache_misaligned_half() {
@@ -258,13 +224,6 @@ mod test {
#[should_panic]
fn cache_misaligned_word() {
let cache = CacheLine::default();
cache.lw(2).unwrap();
}

#[test]
#[should_panic]
fn cache_misaligned_dword() {
let cache = CacheLine::default();
cache.ldw(4).unwrap();
cache.lw(1).unwrap();
}
}
2 changes: 1 addition & 1 deletion vm/src/memory/paged.rs
Original file line number Diff line number Diff line change
@@ -61,7 +61,7 @@ impl Memory for Paged {
let page = addr >> 12;
let offset = ((addr >> 5) & 0x7f) as usize;

const ZERO: CacheLine = CacheLine { dwords: [0; 4] };
const ZERO: CacheLine = CacheLine { words: [0; 8] };
let cl = match self.tree.get(&page) {
None => &ZERO,
Some(arr) => &arr[offset],
74 changes: 34 additions & 40 deletions vm/src/memory/trie.rs
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ use super::Memory;
use crate::circuit::F;
use crate::error::*;

/// A sparse Trie of `CacheLines` with merkle hashing.
/// A sparse Trie of `CacheLines` with merkle hashing..
pub struct MerkleTrie {
// The root node, initially `None`
root: Option<Box<Node>>,
@@ -44,9 +44,24 @@ enum NodeData {
}
use NodeData::*;

// Some (private) convenience methods for projecting out of `NodeData`.
// These methods are only called from contexts in which the match will
// succeed, and only serve to make the code more readable.
// Convenience methods for constructing internal and leaf nodes.
impl Node {
// construct a new leaf node with default data.
fn new_leaf() -> Self {
Self {
digest: Digest::default(),
data: Leaf { val: CacheLine::default() },
}
}

// construct a new internal node with unpopulated children.
fn new_node() -> Self {
Self {
digest: Digest::default(),
data: Branch { left: None, right: None },
}
}
}

impl NodeData {
#[inline]
@@ -82,28 +97,6 @@ impl NodeData {
}
}

// Conveneience methods for constructing internal and leaf nodes.

impl Node {
// construct a new leaf node with default data.
fn new_leaf() -> Self {
Self {
digest: Digest::default(),
data: Leaf { val: CacheLine::default() },
}
}

// construct a new internal node with unpopulated children.
fn new_node() -> Self {
Self {
digest: Digest::default(),
data: Branch { left: None, right: None },
}
}
}

// Conveneience methods for traversing the Trie.

impl Node {
// descend into a child, allocating if necessary
fn descend(&mut self, left: bool, leaf: bool) -> &mut Box<Node> {
@@ -122,7 +115,7 @@ impl Node {
}
match node {
Some(ref mut b) => b,
None => unreachable!(),
None => unimplemented!(),
}
}

@@ -154,17 +147,10 @@ impl Node {
}
}

impl Default for MerkleTrie {
fn default() -> Self {
let params = poseidon_config();
let zeros = compute_zeros(&params).unwrap();
Self { root: None, zeros, params }
}
}

impl MerkleTrie {
// return the current merkle root
fn root(&self) -> Digest {
// return merkle root
#[allow(clippy::question_mark)]
pub fn root(&self) -> Digest {
self.digest(0, &self.root)
}

@@ -179,7 +165,7 @@ impl MerkleTrie {
/// Query the tree at `addr` returning the `CacheLine` (and `Path` if hashes enabled).
/// The default CacheLine is returned if the tree is unpopulated at `addr`.
pub fn query(&self, addr: u32) -> (&CacheLine, Path) {
let addr = addr.reverse_bits() >> IGNORED_BITS;
let addr = addr.reverse_bits();
let mut auth = Vec::new();
let cl = self.query_inner(&self.root, &mut auth, 0, addr);
let path = Path::new(self.root(), cl.scalars(), auth);
@@ -212,14 +198,14 @@ impl MerkleTrie {
where
F: Fn(&mut CacheLine) -> Result<()>,
{
let addr = addr.reverse_bits() >> IGNORED_BITS;
let addr = addr.reverse_bits();
let mut auth = Vec::new();
if self.root.is_none() {
self.root = Some(Box::new(Node::new_node()));
}
let Some(ref mut b) = self.root else { unreachable!() };

// Note: root is never accessed through self in update_,
// Note: root is never accessed through self in update_inner,
// so we can safely make the following optimization
let root = b as *mut Box<Node>;
let root = unsafe { &mut *root as &mut Box<Node> };
@@ -259,6 +245,14 @@ impl MerkleTrie {
}
}

impl Default for MerkleTrie {
fn default() -> Self {
let params = poseidon_config();
let zeros = compute_zeros(&params).unwrap();
Self { root: None, zeros, params }
}
}

impl Memory for MerkleTrie {
type Proof = Path;

64 changes: 60 additions & 4 deletions riscv/src/rv32.rs → vm/src/rv32.rs
Original file line number Diff line number Diff line change
@@ -51,6 +51,12 @@ pub enum AOP {
}
pub use AOP::*;

#[derive(Eq, Hash, PartialEq)]
pub enum InstructionSet {
RV32i,
RV32Nexus,
}

/// RV32 instructions
#[rustfmt::skip]
#[derive(Copy, Clone, Default, Debug, PartialEq)]
@@ -70,8 +76,38 @@ pub enum RV32 {
ALU { aop: AOP, rd: u32, rs1: u32, rs2: u32, },

FENCE,
ECALL,
EBREAK,

// BEGIN RV32Nexus EXTENSION

// ECALL: An overload of the RV32i ECALL instruction, with explicit return.
//
// In the RV32i spec, ECALL looks like:
//
// 00000 00 00000 00000 000 00000 11100 11
//
// We overload the instruction as:
//
// 00000 00 00000 00000 000 {-rd} 11100 11
//
// We then use rd as the return location for the ecall. By making the return
// explicit in this way, circuit generation is much cleaner since the memory
// updates involved are all captured explicitly in the instruction.
ECALL { rd: u32 },

// EBREAK: An overload of the RV32i EBREAK instruction, with explicit return.
//
// In the RV32i spec, EBREAK looks like:
//
// 00000 00 00001 00000 000 00000 11100 11
//
// We overload the instruction as:
//
// 00000 00 00001 00000 000 {-rd} 11100 11
//
// We then use rd as the return location for the ebreak. By making the return
// explicit in this way, circuit generation is much cleaner since the memory
// updates involved are all captured explicitly in the instruction.
EBREAK { rd: u32 },

#[default]
UNIMP,
@@ -88,6 +124,8 @@ impl RV32 {
ALUI { rd, .. } => Some(rd),
ALU { rd, .. } => Some(rd),
LOAD { rd, .. } => Some(rd),
ECALL { rd } => Some(rd),
EBREAK { rd } => Some(rd),
_ => None,
}
}
@@ -148,6 +186,24 @@ impl RV32 {
/// maximum J value
pub const MAX_J: u32 = 42;

pub const fn instruction_set(&self) -> InstructionSet {
match self {
LUI { .. }
| AUIPC { .. }
| JAL { .. }
| JALR { .. }
| BR { .. }
| LOAD { .. }
| STORE { .. }
| ALUI { .. }
| ALU { .. }
| FENCE
| UNIMP => InstructionSet::RV32i,
// we overload these instructions
ECALL { .. } | EBREAK { .. } => InstructionSet::RV32Nexus,
}
}

/// return the J index for instruction
pub const fn index_j(&self) -> u32 {
// It would be nice to use mem::variant_count here,
@@ -201,8 +257,8 @@ impl RV32 {
ALU { aop: AND, .. } => 38,

FENCE => 39,
ECALL => 40,
EBREAK => 41,
ECALL { .. } => 40,
EBREAK { .. } => 41,
UNIMP => 42,
}
}
2 changes: 2 additions & 0 deletions riscv/src/rv32/display.rs → vm/src/rv32/display.rs
Original file line number Diff line number Diff line change
@@ -32,6 +32,8 @@ impl Display for RV32 {
STORE { sop, rs1, rs2, imm } => write!(f, "{} x{}, x{}, {:x}", sop, rs1, rs2, imm),
ALUI { aop, rd, rs1, imm } => write!(f, "{}i x{}, x{}, {:x}", aop, rd, rs1, imm),
ALU { aop, rd, rs1, rs2 } => write!(f, "{} x{}, x{}, x{}", aop, rd, rs1, rs2),
ECALL { rd } => write!(f, "ecall x{}", rd),
EBREAK { rd } => write!(f, "ebreak x{}", rd),
_ => lower(f, self),
}
}
39 changes: 29 additions & 10 deletions riscv/src/rv32/parse.rs → vm/src/rv32/parse.rs
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
use super::*;
use crate::error::*;
use VMError::*;
use NexusVMError::*;

fn bits(val: u32, start: u32, end: u32) -> u32 {
debug_assert!(start <= end);
@@ -158,7 +158,7 @@ mod opcodes {
pub const OPC_ALUI : u32 = 0b_001_0011;
pub const OPC_ALU : u32 = 0b_011_0011;
pub const OPC_FENCE : u32 = 0b_000_1111;
pub const OPC_ECALL : u32 = 0b_111_0011;
pub const OPC_ECALL : u32 = 0b_111_0011; // also captures EBREAK and UNIMP
}
pub use opcodes::*;

@@ -209,12 +209,16 @@ pub(crate) fn parse_u32(word: u32) -> Option<RV32> {

OPC_FENCE => FENCE,

_ => match word {
0x00000073 => ECALL,
0x00100073 => EBREAK,
0xc0001073 => UNIMP, // csrrw x0, cycle, x0
_ => return None,
},
OPC_ECALL => {
match word >> 12 {
0x00000 => ECALL { rd: rd(word) },
0x00100 => EBREAK { rd: rd(word) },
0xc0001 => UNIMP, // csrrw x0, cycle, x0
_ => return None,
}
}

_ => return None,
};
Some(inst)
}
@@ -241,6 +245,15 @@ fn inst_size(b0: u8, b1: u8) -> u32 {
}
}

/// translate RV32i instructions to RV32Nexus instructions
fn translate_nexus(word: u32) -> u32 {
match word {
// ecall // ebreak
0x00000073 | 0x00100073 => word | (0b1010 << 7), // set rd = 10
_ => word,
}
}

/// parse a single instruction from a byte array
pub fn parse_inst(pc: u32, mem: &[u8]) -> Result<Inst> {
if mem.len() < 2 {
@@ -261,6 +274,8 @@ pub fn parse_inst(pc: u32, mem: &[u8]) -> Result<Inst> {
| ((mem[1] as u32) << 8)
| (mem[0] as u32);

let word = translate_nexus(word);

match parse_u32(word) {
None => Err(InvalidInstruction(pc, word)),
Some(inst) => Ok(Inst { pc, len: sz, word, inst }),
@@ -462,11 +477,15 @@ mod test {
}
}

#[test]
fn test_nexus() {
assert_eq!(parse_u32(0x00000573), Some(ECALL { rd: 10 }));
assert_eq!(parse_u32(0x00100573), Some(EBREAK { rd: 10 }));
}

#[test]
fn test_misc() {
assert_eq!(parse_u32(0), None);
assert_eq!(parse_u32(0x00000073), Some(ECALL));
assert_eq!(parse_u32(0x00100073), Some(EBREAK));
assert_eq!(parse_u32(0xc0001073), Some(UNIMP));
}
}
8 changes: 4 additions & 4 deletions vm/src/syscalls.rs
Original file line number Diff line number Diff line change
@@ -4,9 +4,9 @@ use std::collections::VecDeque;
use std::io::Write;

use crate::{
error::{NexusVMError::UnknownSyscall, Result},
instructions::Width,
error::{NexusVMError::UnknownECall, Result},
memory::Memory,
rv32::LOP,
};

/// Holds information related to syscall implementation.
@@ -48,7 +48,7 @@ impl Syscalls {
// write_log
let mut stdout = std::io::stdout();
for addr in inp1..inp1 + inp2 {
let b = memory.load(Width::BU, addr)?.0;
let b = memory.load(LOP::LBU, addr)?.0;
stdout.write_all(&[b as u8])?;
}
let _ = stdout.flush();
@@ -59,7 +59,7 @@ impl Syscalls {
None => out = u32::MAX,
}
} else {
return Err(UnknownSyscall(pc, num));
return Err(UnknownECall(pc, num));
}

Ok(out)
164 changes: 109 additions & 55 deletions vm/src/trace.rs
Original file line number Diff line number Diff line change
@@ -13,13 +13,11 @@
//! step contained in the block. The witnesses can be reconstructed
//! by iterating over the steps in the block.
use num_traits::FromPrimitive;

use crate::circuit::F;
use crate::error::Result;
use crate::eval::{eval_step, NexusVM};
use crate::instructions::{Inst, Opcode::HALT};
use crate::eval::{eval_inst, NexusVM, Regs};
use crate::memory::{Memory, MemoryProof};
use crate::rv32::{parse::*, RV32::UNIMP};

use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use serde::{Deserialize, Serialize};
@@ -38,10 +36,8 @@ pub struct Trace<P: MemoryProof> {
/// A seqeunce of program steps.
#[derive(Default, Clone, Serialize, Deserialize, CanonicalSerialize, CanonicalDeserialize)]
pub struct Block<P: MemoryProof> {
/// Starting program counter for this block.
pub pc: u32,
/// Starting register file for this block.
pub regs: [u32; 32],
pub regs: Regs,
/// Sequence of `k` steps contained in this block.
pub steps: Vec<Step<P>>,
}
@@ -50,7 +46,7 @@ pub struct Block<P: MemoryProof> {
#[derive(Default, Clone, Serialize, Deserialize, CanonicalSerialize, CanonicalDeserialize)]
pub struct Step<P: MemoryProof> {
/// Encoded NexusVM instruction.
pub inst: u64,
pub inst: u32,
/// Result of instruction evaluation.
pub Z: u32,
/// Next program counter, for jump and branch instructions.
@@ -96,12 +92,12 @@ impl<P: MemoryProof> Trace<P> {
}

/// Return the circuit input for block at index `n`.
/// This vector is compatible with the NexusVM step circuit.
/// This vector is compatible with the step circuit.
pub fn input(&self, n: usize) -> Option<Vec<F>> {
let b = self.block(n)?;
let mut v = Vec::new();
v.push(F::from(b.pc));
for x in b.regs {
v.push(F::from(b.regs.pc));
for x in b.regs.x {
v.push(F::from(x));
}
v.push(b.steps[0].pc_proof.commit());
@@ -120,12 +116,16 @@ impl<P: MemoryProof> Trace<P> {

// Generate a `Step` by evaluating the next instruction of `vm`.
fn step<M: Memory>(vm: &mut NexusVM<M>) -> Result<Step<M::Proof>> {
let pc = vm.pc;
eval_step(vm)?;
let pc = vm.regs.pc;
eval_inst(vm)?;
let step = Step {
inst: vm.inst.into(),
inst: vm.inst.word,
Z: vm.Z,
PC: if vm.pc == pc + 8 { None } else { Some(vm.pc) },
PC: if vm.regs.pc == pc + 4 {
None
} else {
Some(vm.regs.pc)
},
pc_proof: vm.pc_proof.clone(),
read_proof: vm.read_proof.clone(),
write_proof: vm.write_proof.clone(),
@@ -135,11 +135,7 @@ fn step<M: Memory>(vm: &mut NexusVM<M>) -> Result<Step<M::Proof>> {

// Generate a `Block` by evaluating `k` steps of `vm`.
fn k_step<M: Memory>(vm: &mut NexusVM<M>, k: usize) -> Result<Block<M::Proof>> {
let mut block = Block {
pc: vm.pc,
regs: vm.regs,
steps: Vec::new(),
};
let mut block = Block { regs: vm.regs.clone(), steps: Vec::new() };

for _ in 0..k {
block.steps.push(step(vm)?);
@@ -150,7 +146,7 @@ fn k_step<M: Memory>(vm: &mut NexusVM<M>, k: usize) -> Result<Block<M::Proof>> {

/// Generate a program trace by evaluating `vm`, using `k` steps
/// per block. If `pow` is true, the total number of steps will
/// be rounded up to the nearest power of two by inserting HALT
/// be rounded up to the nearest power of two by inserting UNIMP
/// instructions.
pub fn trace<M: Memory>(vm: &mut NexusVM<M>, k: usize, pow: bool) -> Result<Trace<M::Proof>> {
let mut trace = Trace { k, start: 0, blocks: Vec::new() };
@@ -159,7 +155,7 @@ pub fn trace<M: Memory>(vm: &mut NexusVM<M>, k: usize, pow: bool) -> Result<Trac
let block = k_step(vm, k)?;
trace.blocks.push(block);

if vm.inst.opcode == HALT {
if vm.inst.inst == UNIMP {
if pow {
let count = trace.blocks.len();
if count.next_power_of_two() == count + 1 {
@@ -176,19 +172,24 @@ pub fn trace<M: Memory>(vm: &mut NexusVM<M>, k: usize, pow: bool) -> Result<Trac
/// Witness for a single VM step.
#[derive(Default, Debug)]
pub struct Witness<P: MemoryProof> {
/// Initial program counter.
pub pc: u32,
/// Initial register file.
pub regs: [u32; 32],
pub regs: Regs,
/// Instruction being executed.
pub inst: Inst,
pub inst: u32,
/// RISC-V instruction components.
pub J: u32,
pub shamt: u32,
pub rs1: u32,
pub rs2: u32,
pub rd: u32,
pub I: u32,
/// First argument value.
pub X: u32,
/// Second argument value.
pub Y: u32,
/// Result of instuction.
pub Z: u32,
/// Next program counter.
/// Program counter.
pub PC: u32,
/// Proof for reading instruction at pc.
pub pc_proof: P,
@@ -214,20 +215,14 @@ impl<'a, P: MemoryProof> IntoIterator for &'a Block<P> {
}

pub struct BlockIter<'a, P: MemoryProof> {
pc: u32,
regs: [u32; 32],
regs: Regs,
block: &'a Block<P>,
index: usize,
}

impl<P: MemoryProof> BlockIter<'_, P> {
fn new(b: &Block<P>) -> BlockIter<'_, P> {
BlockIter {
pc: b.pc,
regs: b.regs,
block: b,
index: 0,
}
BlockIter { regs: b.regs.clone(), block: b, index: 0 }
}
}

@@ -240,49 +235,108 @@ impl<P: MemoryProof> Iterator for BlockIter<'_, P> {
}

let s = &self.block.steps[self.index];
let inst = Inst::from_u64(s.inst)?;
let w = Witness {
pc: self.pc,
regs: self.regs,
inst,
X: self.regs[inst.rs1 as usize],
Y: self.regs[inst.rs2 as usize],
Z: s.Z,
PC: if let Some(pc) = s.PC { pc } else { self.pc + 8 },
pc_proof: s.pc_proof.clone(),
read_proof: s.read_proof.as_ref().unwrap_or(&s.pc_proof).clone(),
write_proof: s.write_proof.as_ref().unwrap_or(&s.pc_proof).clone(),
let inst = parse_u32(s.inst).unwrap();
let mut w = parse_alt(&self.block.regs, s.inst);
w.regs = self.regs.clone();
w.inst = s.inst;
w.J = inst.index_j();
w.X = w.regs.x[w.rs1 as usize];
w.Y = w.regs.x[w.rs2 as usize];
w.Z = s.Z;
w.PC = if let Some(pc) = s.PC {
pc
} else {
self.regs.pc + 4
};
w.pc_proof = s.pc_proof.clone();
w.read_proof = s.read_proof.as_ref().unwrap_or(&w.pc_proof).clone();
w.write_proof = s.write_proof.as_ref().unwrap_or(&w.read_proof).clone();

self.pc = w.PC;
if w.inst.rd > 0 {
self.regs[w.inst.rd as usize] = w.Z;
self.regs.pc = w.PC;
if w.rd > 0 {
self.regs.x[w.rd as usize] = w.Z;
}
self.index += 1;
Some(w)
}
}

fn parse_alt<P: MemoryProof>(regs: &Regs, word: u32) -> Witness<P> {
let mut w = Witness::<P>::default();

match opcode(word) {
OPC_LUI => {
w.rd = rd(word);
w.I = immU(word);
}
OPC_AUIPC => {
w.rd = rd(word);
w.I = immU(word);
}
OPC_JAL => {
w.rd = rd(word);
w.I = immJ(word);
}
OPC_JALR => {
w.rd = rd(word);
w.rs1 = rs1(word);
w.I = immI(word);
}
OPC_BR => {
w.rs1 = rs1(word);
w.rs2 = rs2(word);
w.I = immB(word);
}
OPC_LOAD => {
w.rd = rd(word);
w.rs1 = rs1(word);
w.I = immI(word);
}
OPC_STORE => {
w.rs1 = rs1(word);
w.rs2 = rs2(word);
w.I = immS(word);
}
OPC_ALUI => {
w.rd = rd(word);
w.rs1 = rs1(word);
w.I = immA(word);
w.shamt = w.I & 0x1f;
}
OPC_ALU => {
w.rd = rd(word);
w.rs1 = rs1(word);
w.rs2 = rs2(word);
w.shamt = regs.x[w.rs2 as usize] & 0x1f;
}
OPC_ECALL => {
w.rd = rd(word);
}
_ => (),
};
w
}

#[cfg(test)]
mod test {
use super::*;
use crate::{eval::halt_vm, memory::paged::Paged, memory::trie::MerkleTrie};
use crate::{machines::loop_vm, memory::paged::Paged, memory::trie::MerkleTrie};

// basic check that tracing and iteration succeeds
fn trace_test_machine(mut nvm: NexusVM<impl Memory>) {
let tr = trace(&mut nvm, 1, false).unwrap();
let mut pc = 0u32;
for b in tr.blocks {
for w in b.iter() {
pc = w.pc;
pc = w.regs.pc;
}
}
assert_eq!(nvm.pc, pc);
assert_eq!(nvm.regs.pc, pc);
}

#[test]
fn trace_test_machines() {
trace_test_machine(halt_vm::<Paged>());
trace_test_machine(halt_vm::<MerkleTrie>());
trace_test_machine(loop_vm::<Paged>(5));
trace_test_machine(loop_vm::<MerkleTrie>(5));
}
}