diff --git a/ceno_emul/src/lib.rs b/ceno_emul/src/lib.rs index e60493cb8..d8ec28ab0 100644 --- a/ceno_emul/src/lib.rs +++ b/ceno_emul/src/lib.rs @@ -11,7 +11,7 @@ mod vm_state; pub use vm_state::VMState; mod rv32im; -pub use rv32im::{DecodedInstruction, EmuContext, InsnCodes, InsnFormat, InsnKind}; +pub use rv32im::{DecodedInstruction, EmuContext, InsnCategory, InsnCodes, InsnFormat, InsnKind}; mod elf; pub use elf::Program; diff --git a/ceno_emul/src/platform.rs b/ceno_emul/src/platform.rs index 511981ba0..12ccaea97 100644 --- a/ceno_emul/src/platform.rs +++ b/ceno_emul/src/platform.rs @@ -11,6 +11,8 @@ pub struct Platform { pub rom_end: Addr, pub ram_start: Addr, pub ram_end: Addr, + /// If true, ecall instructions are no-op instead of trap. Testing only. + pub unsafe_ecall_nop: bool, } pub const CENO_PLATFORM: Platform = Platform { @@ -18,6 +20,7 @@ pub const CENO_PLATFORM: Platform = Platform { rom_end: 0x3000_0000 - 1, ram_start: 0x8000_0000, ram_end: 0xFFFF_FFFF, + unsafe_ecall_nop: false, }; impl Platform { diff --git a/ceno_emul/src/rv32im.rs b/ceno_emul/src/rv32im.rs index cd3f24a74..bea4bd5c9 100644 --- a/ceno_emul/src/rv32im.rs +++ b/ceno_emul/src/rv32im.rs @@ -111,7 +111,7 @@ pub struct DecodedInstruction { } #[derive(Clone, Copy, Debug)] -enum InsnCategory { +pub enum InsnCategory { Compute, Branch, Load, @@ -196,7 +196,7 @@ impl InsnKind { pub struct InsnCodes { pub format: InsnFormat, pub kind: InsnKind, - category: InsnCategory, + pub category: InsnCategory, pub(crate) opcode: u32, pub(crate) func3: u32, pub(crate) func7: u32, diff --git a/ceno_emul/src/tracer.rs b/ceno_emul/src/tracer.rs index d17e410a5..7ed36369e 100644 --- a/ceno_emul/src/tracer.rs +++ b/ceno_emul/src/tracer.rs @@ -1,8 +1,9 @@ use std::{collections::HashMap, fmt, mem}; use crate::{ - CENO_PLATFORM, PC_STEP_SIZE, + CENO_PLATFORM, InsnKind, PC_STEP_SIZE, addr::{ByteAddr, Cycle, RegIdx, Word, WordAddr}, + encode_rv32, rv32im::DecodedInstruction, }; @@ -187,6 +188,28 @@ impl StepRecord { ) } + /// Create a test record for an ECALL instruction that can do anything. + pub fn new_ecall_any(cycle: Cycle, pc: ByteAddr) -> StepRecord { + let value = 1234; + Self::new_insn( + cycle, + Change::new(pc, pc + PC_STEP_SIZE), + encode_rv32(InsnKind::EANY, 0, 0, 0, 0), + Some(value), + Some(value), + Some(Change::new(value, value)), + Some(WriteOp { + addr: CENO_PLATFORM.ram_start().into(), + value: Change { + before: value, + after: value, + }, + previous_cycle: 0, + }), + 0, + ) + } + #[allow(clippy::too_many_arguments)] fn new_insn( cycle: Cycle, diff --git a/ceno_emul/src/vm_state.rs b/ceno_emul/src/vm_state.rs index 5ae155fc8..bb7a64ed0 100644 --- a/ceno_emul/src/vm_state.rs +++ b/ceno_emul/src/vm_state.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use super::rv32im::EmuContext; use crate::{ - Program, + PC_STEP_SIZE, Program, addr::{ByteAddr, RegIdx, Word, WordAddr}, platform::Platform, rv32im::{DecodedInstruction, Emulator, TrapCause}, @@ -117,12 +117,21 @@ impl EmuContext for VMState { // Expect an ecall to terminate the program: function HALT with argument exit_code. fn ecall(&mut self) -> Result { let function = self.load_register(self.platform.reg_ecall())?; + let arg0 = self.load_register(self.platform.reg_arg0())?; if function == self.platform.ecall_halt() { - let exit_code = self.load_register(self.platform.reg_arg0())?; - tracing::debug!("halt with exit_code={}", exit_code); + tracing::debug!("halt with exit_code={}", arg0); self.halt(); Ok(true) + } else if self.platform.unsafe_ecall_nop { + // Treat unknown ecalls as all powerful instructions: + // Read two registers, write one register, write one memory word, and branch. + tracing::warn!("ecall ignored: syscall_id={}", function); + self.store_register(DecodedInstruction::RD_NULL as RegIdx, 0)?; + let addr = self.platform.ram_start().into(); + self.store_memory(addr, self.peek_memory(addr))?; + self.set_pc(ByteAddr(self.pc) + PC_STEP_SIZE); + Ok(true) } else { self.trap(TrapCause::EcallError) } diff --git a/ceno_zkvm/src/instructions/riscv.rs b/ceno_zkvm/src/instructions/riscv.rs index 81f8af33c..93b05e743 100644 --- a/ceno_zkvm/src/instructions/riscv.rs +++ b/ceno_zkvm/src/instructions/riscv.rs @@ -9,6 +9,7 @@ pub mod branch; pub mod config; pub mod constants; pub mod divu; +pub mod dummy; pub mod ecall; pub mod jump; pub mod logic; diff --git a/ceno_zkvm/src/instructions/riscv/divu.rs b/ceno_zkvm/src/instructions/riscv/divu.rs index c353a6caf..8d25f583f 100644 --- a/ceno_zkvm/src/instructions/riscv/divu.rs +++ b/ceno_zkvm/src/instructions/riscv/divu.rs @@ -4,6 +4,7 @@ use ff_ext::ExtensionField; use super::{ RIVInstruction, constants::{UINT_LIMBS, UInt}, + dummy::DummyInstruction, r_insn::RInstructionConfig, }; use crate::{ @@ -33,12 +34,30 @@ pub struct ArithConfig { pub struct ArithInstruction(PhantomData<(E, I)>); +pub struct DivOp; +impl RIVInstruction for DivOp { + const INST_KIND: InsnKind = InsnKind::DIV; +} +pub type DivDummy = DummyInstruction; // TODO: implement DivInstruction. + pub struct DivUOp; impl RIVInstruction for DivUOp { const INST_KIND: InsnKind = InsnKind::DIVU; } pub type DivUInstruction = ArithInstruction; +pub struct RemOp; +impl RIVInstruction for RemOp { + const INST_KIND: InsnKind = InsnKind::REM; +} +pub type RemDummy = DummyInstruction; // TODO: implement RemInstruction. + +pub struct RemuOp; +impl RIVInstruction for RemuOp { + const INST_KIND: InsnKind = InsnKind::REMU; +} +pub type RemuDummy = DummyInstruction; // TODO: implement RemuInstruction. + impl Instruction for ArithInstruction { type InstructionConfig = ArithConfig; diff --git a/ceno_zkvm/src/instructions/riscv/dummy/dummy_circuit.rs b/ceno_zkvm/src/instructions/riscv/dummy/dummy_circuit.rs new file mode 100644 index 000000000..1fa7dc4c2 --- /dev/null +++ b/ceno_zkvm/src/instructions/riscv/dummy/dummy_circuit.rs @@ -0,0 +1,266 @@ +use std::marker::PhantomData; + +use ceno_emul::{InsnCategory, InsnFormat, InsnKind, StepRecord}; +use ff_ext::ExtensionField; + +use super::super::{ + RIVInstruction, + constants::UInt, + insn_base::{ReadMEM, ReadRS1, ReadRS2, StateInOut, WriteMEM, WriteRD}, +}; +use crate::{ + circuit_builder::CircuitBuilder, + error::ZKVMError, + expression::{ToExpr, WitIn}, + instructions::Instruction, + set_val, + tables::InsnRecord, + uint::Value, + utils::i64_to_base, + witness::LkMultiplicity, +}; +use core::mem::MaybeUninit; + +/// DummyInstruction can handle any instruction and produce its side-effects. +pub struct DummyInstruction(PhantomData<(E, I)>); + +impl Instruction for DummyInstruction { + type InstructionConfig = DummyConfig; + + fn name() -> String { + format!("{:?}_DUMMY", I::INST_KIND) + } + + fn construct_circuit( + circuit_builder: &mut CircuitBuilder, + ) -> Result { + let codes = I::INST_KIND.codes(); + + // ECALL can do everything. + let is_ecall = matches!(codes.kind, InsnKind::EANY); + + // Regular instructions do what is implied by their format. + let (with_rs1, with_rs2, with_rd) = match codes.format { + _ if is_ecall => (true, true, true), + InsnFormat::R => (true, true, true), + InsnFormat::I => (true, false, true), + InsnFormat::S => (true, true, false), + InsnFormat::B => (true, true, false), + InsnFormat::U => (false, false, true), + InsnFormat::J => (false, false, true), + }; + let with_mem_write = matches!(codes.category, InsnCategory::Store) || is_ecall; + let with_mem_read = matches!(codes.category, InsnCategory::Load); + let branching = matches!(codes.category, InsnCategory::Branch) + || matches!(codes.kind, InsnKind::JAL | InsnKind::JALR) + || is_ecall; + + DummyConfig::construct_circuit( + circuit_builder, + I::INST_KIND, + with_rs1, + with_rs2, + with_rd, + with_mem_write, + with_mem_read, + branching, + ) + } + + fn assign_instance( + config: &Self::InstructionConfig, + instance: &mut [MaybeUninit<::BaseField>], + lk_multiplicity: &mut LkMultiplicity, + step: &StepRecord, + ) -> Result<(), ZKVMError> { + config.assign_instance(instance, lk_multiplicity, step) + } +} + +#[derive(Debug)] +pub struct DummyConfig { + vm_state: StateInOut, + + rs1: Option<(ReadRS1, UInt)>, + rs2: Option<(ReadRS2, UInt)>, + rd: Option<(WriteRD, UInt)>, + + mem_addr_val: Option<[WitIn; 3]>, + mem_read: Option>, + mem_write: Option, + + imm: WitIn, +} + +impl DummyConfig { + #[allow(clippy::too_many_arguments)] + fn construct_circuit( + circuit_builder: &mut CircuitBuilder, + kind: InsnKind, + with_rs1: bool, + with_rs2: bool, + with_rd: bool, + with_mem_write: bool, + with_mem_read: bool, + branching: bool, + ) -> Result { + // State in and out + let vm_state = StateInOut::construct_circuit(circuit_builder, branching)?; + + // Registers + let rs1 = if with_rs1 { + let rs1_read = UInt::new_unchecked(|| "rs1_read", circuit_builder)?; + let rs1_op = + ReadRS1::construct_circuit(circuit_builder, rs1_read.register_expr(), vm_state.ts)?; + Some((rs1_op, rs1_read)) + } else { + None + }; + + let rs2 = if with_rs2 { + let rs2_read = UInt::new_unchecked(|| "rs2_read", circuit_builder)?; + let rs2_op = + ReadRS2::construct_circuit(circuit_builder, rs2_read.register_expr(), vm_state.ts)?; + Some((rs2_op, rs2_read)) + } else { + None + }; + + let rd = if with_rd { + let rd_written = UInt::new_unchecked(|| "rd_written", circuit_builder)?; + let rd_op = WriteRD::construct_circuit( + circuit_builder, + rd_written.register_expr(), + vm_state.ts, + )?; + Some((rd_op, rd_written)) + } else { + None + }; + + // Memory + let mem_addr_val = if with_mem_read || with_mem_write { + Some([ + circuit_builder.create_witin(|| "mem_addr"), + circuit_builder.create_witin(|| "mem_before"), + circuit_builder.create_witin(|| "mem_after"), + ]) + } else { + None + }; + + let mem_read = if with_mem_read { + Some(ReadMEM::construct_circuit( + circuit_builder, + mem_addr_val.as_ref().unwrap()[0].expr(), + mem_addr_val.as_ref().unwrap()[1].expr(), + vm_state.ts, + )?) + } else { + None + }; + + let mem_write = if with_mem_write { + Some(WriteMEM::construct_circuit( + circuit_builder, + mem_addr_val.as_ref().unwrap()[0].expr(), + mem_addr_val.as_ref().unwrap()[1].expr(), + mem_addr_val.as_ref().unwrap()[2].expr(), + vm_state.ts, + )?) + } else { + None + }; + + // Fetch instruction + + // The register IDs of ECALL is fixed, not encoded. + let is_ecall = matches!(kind, InsnKind::EANY); + let rs1_id = match &rs1 { + Some((r, _)) if !is_ecall => r.id.expr(), + _ => 0.into(), + }; + let rs2_id = match &rs2 { + Some((r, _)) if !is_ecall => r.id.expr(), + _ => 0.into(), + }; + let rd_id = match &rd { + Some((r, _)) if !is_ecall => Some(r.id.expr()), + _ => None, + }; + + let imm = circuit_builder.create_witin(|| "imm"); + + circuit_builder.lk_fetch(&InsnRecord::new( + vm_state.pc.expr(), + kind.into(), + rd_id, + rs1_id, + rs2_id, + imm.expr(), + ))?; + + Ok(DummyConfig { + vm_state, + rs1, + rs2, + rd, + mem_addr_val, + mem_read, + mem_write, + imm, + }) + } + + fn assign_instance( + &self, + instance: &mut [MaybeUninit<::BaseField>], + lk_multiplicity: &mut LkMultiplicity, + step: &StepRecord, + ) -> Result<(), ZKVMError> { + // State in and out + self.vm_state.assign_instance(instance, step)?; + + // Fetch instruction + lk_multiplicity.fetch(step.pc().before.0); + + // Registers + if let Some((rs1_op, rs1_read)) = &self.rs1 { + rs1_op.assign_instance(instance, lk_multiplicity, step)?; + + let rs1_val = Value::new_unchecked(step.rs1().expect("rs1 value").value); + rs1_read.assign_value(instance, rs1_val); + } + if let Some((rs2_op, rs2_read)) = &self.rs2 { + rs2_op.assign_instance(instance, lk_multiplicity, step)?; + + let rs2_val = Value::new_unchecked(step.rs2().expect("rs2 value").value); + rs2_read.assign_value(instance, rs2_val); + } + if let Some((rd_op, rd_written)) = &self.rd { + rd_op.assign_instance(instance, lk_multiplicity, step)?; + + let rd_val = Value::new_unchecked(step.rd().expect("rd value").value.after); + rd_written.assign_value(instance, rd_val); + } + + // Memory + if let Some([mem_addr, mem_before, mem_after]) = &self.mem_addr_val { + let mem_op = step.memory_op().expect("memory operation"); + set_val!(instance, mem_addr, u64::from(mem_op.addr)); + set_val!(instance, mem_before, mem_op.value.before as u64); + set_val!(instance, mem_after, mem_op.value.after as u64); + } + if let Some(mem_read) = &self.mem_read { + mem_read.assign_instance(instance, lk_multiplicity, step)?; + } + if let Some(mem_write) = &self.mem_write { + mem_write.assign_instance::(instance, lk_multiplicity, step)?; + } + + let imm = i64_to_base::(InsnRecord::imm_internal(&step.insn())); + set_val!(instance, self.imm, imm); + + Ok(()) + } +} diff --git a/ceno_zkvm/src/instructions/riscv/dummy/mod.rs b/ceno_zkvm/src/instructions/riscv/dummy/mod.rs new file mode 100644 index 000000000..9c912f422 --- /dev/null +++ b/ceno_zkvm/src/instructions/riscv/dummy/mod.rs @@ -0,0 +1,16 @@ +//! Dummy instruction circuits for testing. +//! Support instructions that don’t have a complete implementation yet. +//! It connects all the state together (register writes, etc), but does not verify the values. +//! +//! Usage: +//! Specify an instruction with `trait RIVInstruction` and define a `DummyInstruction` like so: +//! +//! use ceno_zkvm::instructions::riscv::{arith::AddOp, dummy::DummyInstruction}; +//! +//! type AddDummy = DummyInstruction; + +mod dummy_circuit; +pub use dummy_circuit::DummyInstruction; + +#[cfg(test)] +mod test; diff --git a/ceno_zkvm/src/instructions/riscv/dummy/test.rs b/ceno_zkvm/src/instructions/riscv/dummy/test.rs new file mode 100644 index 000000000..df1eb0572 --- /dev/null +++ b/ceno_zkvm/src/instructions/riscv/dummy/test.rs @@ -0,0 +1,101 @@ +use ceno_emul::{Change, InsnKind, StepRecord, encode_rv32}; +use goldilocks::GoldilocksExt2; + +use super::*; +use crate::{ + circuit_builder::{CircuitBuilder, ConstraintSystem}, + instructions::{ + Instruction, + riscv::{arith::AddOp, branch::BeqOp, ecall::EcallDummy}, + }, + scheme::mock_prover::{MOCK_PC_START, MockProver}, +}; + +type AddDummy = DummyInstruction; +type BeqDummy = DummyInstruction; + +#[test] +fn test_dummy_ecall() { + let mut cs = ConstraintSystem::::new(|| "riscv"); + let mut cb = CircuitBuilder::new(&mut cs); + let config = cb + .namespace( + || "ecall_dummy", + |cb| { + let config = EcallDummy::construct_circuit(cb); + Ok(config) + }, + ) + .unwrap() + .unwrap(); + + let step = StepRecord::new_ecall_any(4, MOCK_PC_START); + let insn_code = step.insn_code(); + let (raw_witin, lkm) = + EcallDummy::assign_instances(&config, cb.cs.num_witin as usize, vec![step]).unwrap(); + + MockProver::assert_satisfied_raw(&cb, raw_witin, &[insn_code], None, Some(lkm)); +} + +#[test] +fn test_dummy_r() { + let mut cs = ConstraintSystem::::new(|| "riscv"); + let mut cb = CircuitBuilder::new(&mut cs); + let config = cb + .namespace( + || "add_dummy", + |cb| { + let config = AddDummy::construct_circuit(cb); + Ok(config) + }, + ) + .unwrap() + .unwrap(); + + let insn_code = encode_rv32(InsnKind::ADD, 2, 3, 4, 0); + let (raw_witin, lkm) = AddDummy::assign_instances(&config, cb.cs.num_witin as usize, vec![ + StepRecord::new_r_instruction( + 3, + MOCK_PC_START, + insn_code, + 11, + 0xfffffffe, + Change::new(0, 11_u32.wrapping_add(0xfffffffe)), + 0, + ), + ]) + .unwrap(); + + MockProver::assert_satisfied_raw(&cb, raw_witin, &[insn_code], None, Some(lkm)); +} + +#[test] +fn test_dummy_b() { + let mut cs = ConstraintSystem::::new(|| "riscv"); + let mut cb = CircuitBuilder::new(&mut cs); + let config = cb + .namespace( + || "beq_dummy", + |cb| { + let config = BeqDummy::construct_circuit(cb); + Ok(config) + }, + ) + .unwrap() + .unwrap(); + + let insn_code = encode_rv32(InsnKind::BEQ, 2, 3, 0, 8); + let (raw_witin, lkm) = BeqDummy::assign_instances(&config, cb.cs.num_witin as usize, vec![ + StepRecord::new_b_instruction( + 3, + Change::new(MOCK_PC_START, MOCK_PC_START + 8_usize), + insn_code, + 0xbead1010, + 0xbead1010, + 0, + ), + ]) + .unwrap(); + + MockProver::assert_satisfied_raw(&cb, raw_witin, &[insn_code], None, Some(lkm)); +} diff --git a/ceno_zkvm/src/instructions/riscv/ecall.rs b/ceno_zkvm/src/instructions/riscv/ecall.rs index 76c1c04e6..0d7a3315a 100644 --- a/ceno_zkvm/src/instructions/riscv/ecall.rs +++ b/ceno_zkvm/src/instructions/riscv/ecall.rs @@ -1,3 +1,13 @@ mod halt; +use ceno_emul::InsnKind; pub use halt::HaltInstruction; + +use super::{RIVInstruction, dummy::DummyInstruction}; + +pub struct EcallOp; +impl RIVInstruction for EcallOp { + const INST_KIND: InsnKind = InsnKind::EANY; +} +/// Unsafe. A dummy ecall circuit that ignores unimplemented functions. +pub type EcallDummy = DummyInstruction; diff --git a/ceno_zkvm/src/instructions/riscv/insn_base.rs b/ceno_zkvm/src/instructions/riscv/insn_base.rs index 79f07b947..9588cb34f 100644 --- a/ceno_zkvm/src/instructions/riscv/insn_base.rs +++ b/ceno_zkvm/src/instructions/riscv/insn_base.rs @@ -111,16 +111,15 @@ impl ReadRS1 { lk_multiplicity: &mut LkMultiplicity, step: &StepRecord, ) -> Result<(), ZKVMError> { - set_val!(instance, self.id, step.insn().rs1() as u64); - - // Register state - set_val!(instance, self.prev_ts, step.rs1().unwrap().previous_cycle); + let op = step.rs1().expect("rs1 op"); + set_val!(instance, self.id, op.register_index() as u64); + set_val!(instance, self.prev_ts, op.previous_cycle); // Register read self.lt_cfg.assign_instance( instance, lk_multiplicity, - step.rs1().unwrap().previous_cycle, + op.previous_cycle, step.cycle() + Tracer::SUBCYCLE_RS1, )?; @@ -166,16 +165,15 @@ impl ReadRS2 { lk_multiplicity: &mut LkMultiplicity, step: &StepRecord, ) -> Result<(), ZKVMError> { - set_val!(instance, self.id, step.insn().rs2() as u64); - - // Register state - set_val!(instance, self.prev_ts, step.rs2().unwrap().previous_cycle); + let op = step.rs2().expect("rs2 op"); + set_val!(instance, self.id, op.register_index() as u64); + set_val!(instance, self.prev_ts, op.previous_cycle); // Register read self.lt_cfg.assign_instance( instance, lk_multiplicity, - step.rs2().unwrap().previous_cycle, + op.previous_cycle, step.cycle() + Tracer::SUBCYCLE_RS2, )?; @@ -223,20 +221,21 @@ impl WriteRD { lk_multiplicity: &mut LkMultiplicity, step: &StepRecord, ) -> Result<(), ZKVMError> { - set_val!(instance, self.id, step.insn().rd_internal() as u64); - set_val!(instance, self.prev_ts, step.rd().unwrap().previous_cycle); + let op = step.rd().expect("rd op"); + set_val!(instance, self.id, op.register_index() as u64); + set_val!(instance, self.prev_ts, op.previous_cycle); // Register state self.prev_value.assign_limbs( instance, - Value::new_unchecked(step.rd().unwrap().value.before).as_u16_limbs(), + Value::new_unchecked(op.value.before).as_u16_limbs(), ); // Register write self.lt_cfg.assign_instance( instance, lk_multiplicity, - step.rd().unwrap().previous_cycle, + op.previous_cycle, step.cycle() + Tracer::SUBCYCLE_RD, )?;