diff --git a/cartesi-rollups/node/blockchain-reader/src/test_utils.rs b/cartesi-rollups/node/blockchain-reader/src/test_utils.rs index 9b20bfce..e618dc02 100644 --- a/cartesi-rollups/node/blockchain-reader/src/test_utils.rs +++ b/cartesi-rollups/node/blockchain-reader/src/test_utils.rs @@ -9,13 +9,10 @@ use alloy::{ signers::{Signer, local::PrivateKeySigner}, }; use cartesi_dave_contracts::i_dave_app_factory::IDaveAppFactory::{self, WithdrawalConfig}; +use cartesi_machine::{Machine, config::runtime::RuntimeConfig}; use cartesi_rollups_contracts::i_input_box::IInputBox; use serde::Deserialize; -use std::{ - fs::{self, File}, - io::{Read, Seek}, - path::PathBuf, -}; +use std::{fs, path::PathBuf}; type Result = std::result::Result>; @@ -76,15 +73,19 @@ pub async fn spawn_anvil_and_provider() -> Result<(AnvilInstance, DynProvider, A let input_box = deployment_address("InputBox"); let dave_app_factory = deployment_address("DaveAppFactory"); - let initial_hash = { - // Root hash is stored in hash_tree.sht at offset 0x60 (node 1's hash in sparse tree). - // Equivalent to: xxd -seek 0x60 -l 0x20 -c 0x20 -p .../machine-image/hash_tree.sht - let mut file = - File::open(program_path.join("machine-image").join("hash_tree.sht")).unwrap(); - file.seek(std::io::SeekFrom::Start(0x60)).unwrap(); - let mut buffer = [0u8; 32]; - file.read_exact(&mut buffer).unwrap(); - buffer + // Load the stored machine through the emulator and ask it for the root + // hash, rather than reading the internal `hash_tree.sht` file directly. + // The file layout is an emulator implementation detail; going through + // `cm_load_new` + `cm_get_root_hash` is the only stable API. + let initial_hash: [u8; 32] = { + let mut machine = Machine::load( + &program_path.join("machine-image"), + &RuntimeConfig::quiet_console(), + ) + .expect("failed to load stored machine"); + machine + .root_hash() + .expect("failed to read machine root hash") }; let withdrawal_config = WithdrawalConfig { diff --git a/cartesi-rollups/node/state-manager/src/rollups_machine.rs b/cartesi-rollups/node/state-manager/src/rollups_machine.rs index 8c0a9d5f..2c6473b2 100644 --- a/cartesi-rollups/node/state-manager/src/rollups_machine.rs +++ b/cartesi-rollups/node/state-manager/src/rollups_machine.rs @@ -4,13 +4,14 @@ use std::path::{Path, PathBuf}; use cartesi_prt_core::machine::constants::{ - LOG2_BARCH_SPAN_TO_INPUT, LOG2_INPUT_SPAN_TO_EPOCH, LOG2_UARCH_SPAN_TO_BARCH, + CHECKPOINT_ADDRESS, LOG2_BARCH_SPAN_TO_INPUT, LOG2_INPUT_SPAN_TO_EPOCH, + LOG2_UARCH_SPAN_TO_BARCH, }; use crate::{CommitmentLeaf, Proof}; use cartesi_machine::{ - config::runtime::{HTIFRuntimeConfig, RuntimeConfig}, - constants::{break_reason, machine::TREE_LOG2_ROOT_SIZE, pma::TX_START}, + config::runtime::RuntimeConfig, + constants::{ar::TX_START, break_reason, machine::HASH_TREE_LOG2_ROOT_SIZE}, error::{MachineError, MachineResult}, machine::Machine, types::{ @@ -45,8 +46,6 @@ pub const STRIDE_COUNT_IN_EPOCH: u64 = 1 << (LOG2_INPUT_SPAN_TO_EPOCH + LOG2_BARCH_SPAN_TO_INPUT + LOG2_UARCH_SPAN_TO_BARCH - LOG2_STRIDE); -pub const CHECKPOINT_ADDRESS: u64 = 0xfe0; - pub struct RollupsMachine { machine: Machine, epoch_number: u64, @@ -59,12 +58,7 @@ impl RollupsMachine { epoch_number: u64, next_input_index_in_epoch: u64, ) -> MachineResult { - let runtime_config = RuntimeConfig { - htif: Some(HTIFRuntimeConfig { - no_console_putchar: Some(true), - }), - ..Default::default() - }; + let runtime_config = RuntimeConfig::quiet_console(); let machine = Machine::load(path, &runtime_config)?; Ok(Self { @@ -88,7 +82,7 @@ impl RollupsMachine { } pub fn outputs_proof(&mut self) -> MachineResult<(Hash, Proof)> { - let proof = self.machine.proof(TX_START, 5, TREE_LOG2_ROOT_SIZE)?; + let proof = self.machine.proof(TX_START, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let siblings = Proof::new(proof.sibling_hashes); let output_merkle = self.machine.read_memory(TX_START, 32)?; diff --git a/machine/rust-bindings/cartesi-machine/src/config/machine.rs b/machine/rust-bindings/cartesi-machine/src/config/machine.rs index 5d3b6dde..0905483b 100644 --- a/machine/rust-bindings/cartesi-machine/src/config/machine.rs +++ b/machine/rust-bindings/cartesi-machine/src/config/machine.rs @@ -1,342 +1,239 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) +//! Rust mirror of `cartesi::machine_config` and friends from the v0.20 +//! cartesi-machine C++ API. The field names and nesting match the JSON +//! emitted by `cm_get_default_config` / `cm_get_initial_config` and consumed +//! by `cm_create_new`. +//! +//! Invariants this module tries to enforce: +//! +//! 1. **Exact shape match with the C++ side.** Every struct carries +//! `#[serde(deny_unknown_fields)]` so that a future emulator release that +//! adds or renames a field causes a *loud* deserialization failure rather +//! than silent data loss. +//! 2. **No speculative `#[serde(default)]`.** The C++ `to_json` functions +//! always emit every field, so missing fields on deserialize indicate a +//! schema break, not a tolerable omission. Defaults are only applied on +//! types the user *constructs* from scratch (via `Default::default()`), +//! not on fields that participate in JSON round-trips. +//! 3. **Round-trip equality.** `serde_json::from_str::(raw) -> +//! serde_json::to_value` must equal `serde_json::from_str::(raw)` +//! for any JSON produced by the C library. Verified by +//! `test_default_config_json_roundtrip`. + use serde::{Deserialize, Serialize}; use std::path::PathBuf; -/// Backing store config; matches C++ backing_store_config (data_filename, etc.). -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +// --------------------------------------------------------------------------- +// Backing store +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::backing_store_config`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct BackingStoreConfig { - #[serde(default)] pub shared: bool, - #[serde(default)] pub create: bool, - #[serde(default)] pub truncate: bool, - #[serde(default)] pub data_filename: PathBuf, - #[serde(default)] pub dht_filename: PathBuf, - #[serde(default)] pub dpt_filename: PathBuf, } -/// Config with only backing_store; matches C++ backing_store_config_only. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +/// Mirror of C++ `cartesi::backing_store_config_only`. Used for memory +/// regions that have no extra per-range config (pmas, uarch ram, cmio rx/tx). +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct BackingStoreConfigOnly { - #[serde(default)] pub backing_store: BackingStoreConfig, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MachineConfig { - pub processor: ProcessorConfig, - pub ram: RAMConfig, - pub dtb: DTBConfig, - pub flash_drive: FlashDriveConfigs, - #[serde(default)] - pub tlb: TLBConfig, - #[serde(default)] - pub clint: CLINTConfig, - #[serde(default)] - pub plic: PLICConfig, - #[serde(default)] - pub htif: HTIFConfig, - pub uarch: UarchConfig, - pub cmio: CmioConfig, - pub virtio: VirtIOConfigs, +// --------------------------------------------------------------------------- +// Register substructures (all nested inside RegistersConfig) +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::iflags_state`. The field names in JSON are the +/// uppercase single letters `X`, `Y`, `H`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct IFlagsConfig { + #[serde(rename = "X")] + pub x: u64, + #[serde(rename = "Y")] + pub y: u64, + #[serde(rename = "H")] + pub h: u64, } -impl MachineConfig { - pub fn new_with_ram(ram: RAMConfig) -> Self { - Self { - processor: ProcessorConfig::default(), - ram, - dtb: DTBConfig::default(), - flash_drive: FlashDriveConfigs::default(), - tlb: TLBConfig::default(), - clint: CLINTConfig::default(), - plic: PLICConfig::default(), - htif: HTIFConfig::default(), - uarch: UarchConfig::default(), - cmio: CmioConfig::default(), - virtio: VirtIOConfigs::default(), - } - } - - pub fn processor(mut self, processor: ProcessorConfig) -> Self { - self.processor = processor; - self - } - - pub fn dtb(mut self, dtb: DTBConfig) -> Self { - self.dtb = dtb; - self - } - - pub fn add_flash_drive(mut self, flash_drive: MemoryRangeConfig) -> Self { - self.flash_drive.push(flash_drive); - self - } - - pub fn tlb(mut self, tlb: TLBConfig) -> Self { - self.tlb = tlb; - self - } - - pub fn clint(mut self, clint: CLINTConfig) -> Self { - self.clint = clint; - self - } - - pub fn plic(mut self, plic: PLICConfig) -> Self { - self.plic = plic; - self - } - - pub fn htif(mut self, htif: HTIFConfig) -> Self { - self.htif = htif; - self - } - - pub fn uarch(mut self, uarch: UarchConfig) -> Self { - self.uarch = uarch; - self - } - - pub fn cmio(mut self, cmio: CmioConfig) -> Self { - self.cmio = cmio; - self - } +/// Mirror of C++ `cartesi::clint_state`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct CLINTConfig { + pub mtimecmp: u64, +} - pub fn add_virtio(mut self, virtio_config: VirtIODeviceConfig) -> Self { - self.virtio.push(virtio_config); - self - } +/// Mirror of C++ `cartesi::plic_state`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct PLICConfig { + pub girqpend: u64, + pub girqsrvd: u64, } -fn default_config() -> MachineConfig { - crate::machine::Machine::default_config().expect("failed to get default config") +/// Mirror of C++ `cartesi::htif_state`. These are the five HTIF CSRs; the +/// old Rust binding used a different HTIF-runtime struct with feature flags +/// (`console_getchar`, `yield_manual`, `yield_automatic`), which belong to +/// `RuntimeConfig`, not `MachineConfig`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct HTIFConfig { + pub fromhost: u64, + pub tohost: u64, + pub ihalt: u64, + pub iconsole: u64, + pub iyield: u64, } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ProcessorConfig { - #[serde(default)] - pub backing_store: BackingStoreConfig, - #[serde(default)] +/// Mirror of C++ `cartesi::registers_state`. This is the object emitted at +/// `processor.registers` in the config JSON. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct RegistersConfig { pub x0: u64, - #[serde(default)] pub x1: u64, - #[serde(default)] pub x2: u64, - #[serde(default)] pub x3: u64, - #[serde(default)] pub x4: u64, - #[serde(default)] pub x5: u64, - #[serde(default)] pub x6: u64, - #[serde(default)] pub x7: u64, - #[serde(default)] pub x8: u64, - #[serde(default)] pub x9: u64, - #[serde(default)] pub x10: u64, - #[serde(default)] pub x11: u64, - #[serde(default)] pub x12: u64, - #[serde(default)] pub x13: u64, - #[serde(default)] pub x14: u64, - #[serde(default)] pub x15: u64, - #[serde(default)] pub x16: u64, - #[serde(default)] pub x17: u64, - #[serde(default)] pub x18: u64, - #[serde(default)] pub x19: u64, - #[serde(default)] pub x20: u64, - #[serde(default)] pub x21: u64, - #[serde(default)] pub x22: u64, - #[serde(default)] pub x23: u64, - #[serde(default)] pub x24: u64, - #[serde(default)] pub x25: u64, - #[serde(default)] pub x26: u64, - #[serde(default)] pub x27: u64, - #[serde(default)] pub x28: u64, - #[serde(default)] pub x29: u64, - #[serde(default)] pub x30: u64, - #[serde(default)] pub x31: u64, - #[serde(default)] pub f0: u64, - #[serde(default)] pub f1: u64, - #[serde(default)] pub f2: u64, - #[serde(default)] pub f3: u64, - #[serde(default)] pub f4: u64, - #[serde(default)] pub f5: u64, - #[serde(default)] pub f6: u64, - #[serde(default)] pub f7: u64, - #[serde(default)] pub f8: u64, - #[serde(default)] pub f9: u64, - #[serde(default)] pub f10: u64, - #[serde(default)] pub f11: u64, - #[serde(default)] pub f12: u64, - #[serde(default)] pub f13: u64, - #[serde(default)] pub f14: u64, - #[serde(default)] pub f15: u64, - #[serde(default)] pub f16: u64, - #[serde(default)] pub f17: u64, - #[serde(default)] pub f18: u64, - #[serde(default)] pub f19: u64, - #[serde(default)] pub f20: u64, - #[serde(default)] pub f21: u64, - #[serde(default)] pub f22: u64, - #[serde(default)] pub f23: u64, - #[serde(default)] pub f24: u64, - #[serde(default)] pub f25: u64, - #[serde(default)] pub f26: u64, - #[serde(default)] pub f27: u64, - #[serde(default)] pub f28: u64, - #[serde(default)] pub f29: u64, - #[serde(default)] pub f30: u64, - #[serde(default)] pub f31: u64, - #[serde(default)] pub pc: u64, - #[serde(default)] pub fcsr: u64, - #[serde(default)] pub mvendorid: u64, - #[serde(default)] pub marchid: u64, - #[serde(default)] pub mimpid: u64, - #[serde(default)] pub mcycle: u64, - #[serde(default)] pub icycleinstret: u64, - #[serde(default)] pub mstatus: u64, - #[serde(default)] pub mtvec: u64, - #[serde(default)] pub mscratch: u64, - #[serde(default)] pub mepc: u64, - #[serde(default)] pub mcause: u64, - #[serde(default)] pub mtval: u64, - #[serde(default)] pub misa: u64, - #[serde(default)] pub mie: u64, - #[serde(default)] pub mip: u64, - #[serde(default)] pub medeleg: u64, - #[serde(default)] pub mideleg: u64, - #[serde(default)] pub mcounteren: u64, - #[serde(default)] pub menvcfg: u64, - #[serde(default)] pub stvec: u64, - #[serde(default)] pub sscratch: u64, - #[serde(default)] pub sepc: u64, - #[serde(default)] pub scause: u64, - #[serde(default)] pub stval: u64, - #[serde(default)] pub satp: u64, - #[serde(default)] pub scounteren: u64, - #[serde(default)] pub senvcfg: u64, - #[serde(default)] pub ilrsc: u64, - #[serde(default)] pub iprv: u64, - #[serde(default)] - #[serde(rename = "iflags_X")] - pub iflags_x: u64, - #[serde(default)] - #[serde(rename = "iflags_Y")] - pub iflags_y: u64, - #[serde(default)] - #[serde(rename = "iflags_H")] - pub iflags_h: u64, - #[serde(default)] + pub iflags: IFlagsConfig, pub iunrep: u64, + pub clint: CLINTConfig, + pub plic: PLICConfig, + pub htif: HTIFConfig, +} + +// --------------------------------------------------------------------------- +// Processor +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::processor_config`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ProcessorConfig { + pub registers: RegistersConfig, + pub backing_store: BackingStoreConfig, } impl Default for ProcessorConfig { fn default() -> Self { - default_config().processor + library_default().processor } } -#[derive(Clone, Debug, Serialize, Deserialize)] +// --------------------------------------------------------------------------- +// RAM and DTB +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::ram_config`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct RAMConfig { pub length: u64, pub backing_store: BackingStoreConfig, } -#[derive(Clone, Debug, Serialize, Deserialize)] +/// Mirror of C++ `cartesi::dtb_config`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct DTBConfig { pub bootargs: String, pub init: String, @@ -346,190 +243,178 @@ pub struct DTBConfig { impl Default for DTBConfig { fn default() -> Self { - default_config().dtb + library_default().dtb } } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +// --------------------------------------------------------------------------- +// Memory range / flash drive +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::memory_range_config`. The C++ side uses the +/// sentinel `UINT64_MAX` for "auto-detect" on `start`/`length`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct MemoryRangeConfig { - #[serde(skip_serializing_if = "Option::is_none", default)] - pub start: Option, - #[serde(skip_serializing_if = "Option::is_none", default)] - pub length: Option, - #[serde(default)] + pub start: u64, + pub length: u64, pub read_only: bool, pub backing_store: BackingStoreConfig, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct CmioBufferConfig { - pub backing_store: BackingStoreConfig, +impl Default for MemoryRangeConfig { + /// Defaults match the C++ `memory_range_config` in-struct initializers: + /// `start` and `length` are `UINT64_MAX` to mean "auto-detect". + fn default() -> Self { + Self { + start: u64::MAX, + length: u64::MAX, + read_only: false, + backing_store: BackingStoreConfig::default(), + } + } +} + +pub type FlashDriveConfigs = Vec; + +// --------------------------------------------------------------------------- +// CMIO +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::cmio_config`. Both buffers are +/// `backing_store_config_only`. +pub type CmioBufferConfig = BackingStoreConfigOnly; + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct CmioConfig { + pub rx_buffer: CmioBufferConfig, + pub tx_buffer: CmioBufferConfig, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct VirtIOHostfwd { +// --------------------------------------------------------------------------- +// VirtIO +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::virtio_hostfwd_config`. Note that `host_port` +/// and `guest_port` are `uint16_t` on the C++ side. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct VirtIOHostfwdConfig { pub is_udp: bool, pub host_ip: u64, pub guest_ip: u64, - pub host_port: u64, - pub guest_port: u64, + pub host_port: u16, + pub guest_port: u16, } -pub type VirtIOHostfwdArray = Vec; +pub type VirtIOHostfwdArray = Vec; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum VirtIODeviceType { - #[default] +/// Mirror of C++ `cartesi::virtio_device_config` (a `std::variant`). The +/// JSON representation uses `"type"` as the discriminator, matching the +/// `to_json(virtio_device_config)` implementation. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "type", rename_all = "kebab-case", deny_unknown_fields)] +pub enum VirtIODeviceConfig { Console, - P9fs, + P9fs { + tag: String, + host_directory: String, + }, #[serde(rename = "net-user")] - NetUser, + NetUser { + hostfwd: VirtIOHostfwdArray, + }, #[serde(rename = "net-tuntap")] - NetTuntap, + NetTuntap { + iface: String, + }, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct VirtIODeviceConfig { - pub r#type: VirtIODeviceType, - pub tag: String, - pub host_directory: String, - pub hostfwd: VirtIOHostfwdArray, - pub iface: String, +impl Default for VirtIODeviceConfig { + fn default() -> Self { + VirtIODeviceConfig::Console + } } -pub type FlashDriveConfigs = Vec; +pub type VirtIOConfigs = Vec; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct TLBConfig { - #[serde(default)] - pub backing_store: BackingStoreConfig, -} +// --------------------------------------------------------------------------- +// PMAS +// --------------------------------------------------------------------------- -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct CLINTConfig { - #[serde(default)] - pub mtimecmp: u64, -} +/// Mirror of C++ `cartesi::pmas_config` (alias for `backing_store_config_only`). +pub type PmasConfig = BackingStoreConfigOnly; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct PLICConfig { - #[serde(default)] - pub girqpend: u64, - #[serde(default)] - pub girqsrvd: u64, -} +// --------------------------------------------------------------------------- +// Uarch +// --------------------------------------------------------------------------- -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct HTIFConfig { - #[serde(default)] - pub fromhost: u64, - #[serde(default)] - pub tohost: u64, - #[serde(default)] - pub console_getchar: bool, - #[serde(default)] - pub yield_manual: bool, - #[serde(default)] - pub yield_automatic: bool, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UarchProcessorConfig { - #[serde(default)] - pub backing_store: BackingStoreConfig, - #[serde(default)] +/// Mirror of C++ `cartesi::uarch_registers_state`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct UarchRegistersConfig { pub x0: u64, - #[serde(default)] pub x1: u64, - #[serde(default)] pub x2: u64, - #[serde(default)] pub x3: u64, - #[serde(default)] pub x4: u64, - #[serde(default)] pub x5: u64, - #[serde(default)] pub x6: u64, - #[serde(default)] pub x7: u64, - #[serde(default)] pub x8: u64, - #[serde(default)] pub x9: u64, - #[serde(default)] pub x10: u64, - #[serde(default)] pub x11: u64, - #[serde(default)] pub x12: u64, - #[serde(default)] pub x13: u64, - #[serde(default)] pub x14: u64, - #[serde(default)] pub x15: u64, - #[serde(default)] pub x16: u64, - #[serde(default)] pub x17: u64, - #[serde(default)] pub x18: u64, - #[serde(default)] pub x19: u64, - #[serde(default)] pub x20: u64, - #[serde(default)] pub x21: u64, - #[serde(default)] pub x22: u64, - #[serde(default)] pub x23: u64, - #[serde(default)] pub x24: u64, - #[serde(default)] pub x25: u64, - #[serde(default)] pub x26: u64, - #[serde(default)] pub x27: u64, - #[serde(default)] pub x28: u64, - #[serde(default)] pub x29: u64, - #[serde(default)] pub x30: u64, - #[serde(default)] pub x31: u64, - #[serde(default)] pub pc: u64, - #[serde(default)] pub cycle: u64, - #[serde(default)] - pub halt_flag: bool, + /// `uint64_t` on the C++ side (shadow-uarch-state.h), not a C++ `bool`. + /// Used as a boolean flag (0 = not halted, non-zero = halted), but the + /// wire representation is an integer. + pub halt_flag: u64, } -impl Default for UarchProcessorConfig { - fn default() -> Self { - default_config().uarch.processor - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UarchRAMConfig { - #[serde(skip_serializing_if = "Option::is_none", default)] - pub length: Option, +/// Mirror of C++ `cartesi::uarch_processor_config`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct UarchProcessorConfig { + pub registers: UarchRegistersConfig, pub backing_store: BackingStoreConfig, } -impl Default for UarchRAMConfig { +impl Default for UarchProcessorConfig { fn default() -> Self { - default_config().uarch.ram + library_default().uarch.processor } } -#[derive(Clone, Debug, Serialize, Deserialize)] +/// Mirror of C++ `cartesi::uarch_ram_config` (alias for +/// `backing_store_config_only`). +pub type UarchRAMConfig = BackingStoreConfigOnly; + +/// Mirror of C++ `cartesi::uarch_config`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct UarchConfig { pub processor: UarchProcessorConfig, pub ram: UarchRAMConfig, @@ -537,40 +422,174 @@ pub struct UarchConfig { impl Default for UarchConfig { fn default() -> Self { - default_config().uarch + library_default().uarch } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CmioConfig { - pub rx_buffer: CmioBufferConfig, - pub tx_buffer: CmioBufferConfig, +// --------------------------------------------------------------------------- +// Hash tree +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::hash_function_type`. Serialized as a lower-case +/// string (`"keccak256"` / `"sha256"`). +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum HashFunctionType { + #[default] + Keccak256, + Sha256, } -impl Default for CmioConfig { +/// Mirror of C++ `cartesi::hash_tree_config`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct HashTreeConfig { + pub shared: bool, + pub create: bool, + pub sht_filename: PathBuf, + pub phtc_filename: PathBuf, + pub phtc_size: u64, + pub hash_function: HashFunctionType, +} + +impl Default for HashTreeConfig { fn default() -> Self { - default_config().cmio + library_default().hash_tree } } -pub type VirtIOConfigs = Vec; +// --------------------------------------------------------------------------- +// Top-level machine config +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::machine_config`. The field ordering matches the +/// order in which `to_json(machine_config)` emits them on the C++ side. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct MachineConfig { + pub processor: ProcessorConfig, + pub ram: RAMConfig, + pub dtb: DTBConfig, + pub flash_drive: FlashDriveConfigs, + pub virtio: VirtIOConfigs, + pub cmio: CmioConfig, + pub pmas: PmasConfig, + pub uarch: UarchConfig, + pub hash_tree: HashTreeConfig, +} + +impl MachineConfig { + /// Starts from the library's default config and overrides only the RAM + /// block. Useful for the common case where callers want the emulator's + /// baseline configuration plus a specific RAM image. + pub fn new_with_ram(ram: RAMConfig) -> Self { + let mut cfg = library_default(); + cfg.ram = ram; + cfg + } +} + +/// Fetches the emulator's built-in default config via `cm_get_default_config`. +/// All `Default` impls in this file delegate here rather than synthesizing +/// zeros in Rust, because C-side defaults carry non-trivial values like +/// `mvendorid`, `marchid`, initial `misa`, and the DTB `bootargs` string. +fn library_default() -> MachineConfig { + crate::machine::Machine::default_config() + .expect("failed to get default machine config from cartesi-machine library") +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; + use crate::machine::Machine; + use crate::{EXPECTED_EMULATOR_VERSION, format_emulator_version}; + + /// Guardrail: the linked `libcartesi` must report the exact version these + /// bindings were written against. If this fails after an emulator bump, + /// update `EXPECTED_EMULATOR_VERSION` in `lib.rs` and rerun the config + /// round-trip tests to re-confirm the schema. + #[test] + fn test_emulator_version_pin() { + let linked = Machine::version(); + assert_eq!( + linked, + EXPECTED_EMULATOR_VERSION, + "cartesi-machine bindings were written for emulator version {}, but libcartesi reports {}. \ + Update EXPECTED_EMULATOR_VERSION after verifying the config schema still matches.", + format_emulator_version(EXPECTED_EMULATOR_VERSION), + format_emulator_version(linked), + ); + } #[test] fn test_default_configs() { - default_config(); + library_default(); ProcessorConfig::default(); DTBConfig::default(); - TLBConfig::default(); + MemoryRangeConfig::default(); CLINTConfig::default(); PLICConfig::default(); HTIFConfig::default(); UarchProcessorConfig::default(); - UarchRAMConfig::default(); UarchConfig::default(); CmioConfig::default(); + HashTreeConfig::default(); + } + + /// Guardrail against silent schema drift between the Rust bindings and + /// the C++ `cartesi::machine_config`. Loads the default config as raw + /// JSON, deserializes it into `MachineConfig`, re-serializes, and + /// asserts structural equality with the original JSON. + /// + /// If this test fails after an emulator bump, do NOT add + /// `#[serde(default)]` to make it pass — the right fix is to update + /// this file's structs to match whatever the C++ side now emits. + #[test] + fn test_default_config_json_roundtrip() { + let raw_json = + Machine::default_config_raw_json().expect("failed to fetch raw default config JSON"); + + let original: serde_json::Value = serde_json::from_str(&raw_json) + .expect("raw JSON from cm_get_default_config is not valid JSON"); + + let typed: MachineConfig = serde_json::from_str(&raw_json).unwrap_or_else(|e| { + panic!( + "failed to deserialize cm_get_default_config JSON into MachineConfig: {e}\n\ + (this usually means a schema drift between the emulator and these bindings)" + ); + }); + + let reserialized = serde_json::to_value(&typed).expect("re-serialization failed"); + + assert_eq!( + original, reserialized, + "MachineConfig round-trip lost or added data. Schema drift vs the C++ side." + ); + } + + /// Guardrail: makes sure an unknown field at the top level fails rather + /// than being silently dropped. Regression test for the bug this + /// refactor is fixing. + #[test] + fn test_unknown_field_is_rejected() { + let raw_json = + Machine::default_config_raw_json().expect("failed to fetch raw default config JSON"); + + // Inject an unknown top-level field. + let mut value: serde_json::Value = serde_json::from_str(&raw_json).unwrap(); + value + .as_object_mut() + .unwrap() + .insert("something_new".to_string(), serde_json::json!(42)); + + let result = serde_json::from_value::(value); + assert!( + result.is_err(), + "deny_unknown_fields must reject previously-unseen top-level keys" + ); } } diff --git a/machine/rust-bindings/cartesi-machine/src/config/runtime.rs b/machine/rust-bindings/cartesi-machine/src/config/runtime.rs index 6f131d4a..2c7d191b 100644 --- a/machine/rust-bindings/cartesi-machine/src/config/runtime.rs +++ b/machine/rust-bindings/cartesi-machine/src/config/runtime.rs @@ -1,32 +1,251 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) +//! Rust mirror of `cartesi::machine_runtime_config` from the v0.20 cartesi-machine +//! C++ API. Follows the same invariants as `config::machine`: +//! +//! 1. Every struct carries `#[serde(deny_unknown_fields)]` to surface future +//! schema additions as explicit deserialization failures. +//! 2. No speculative `#[serde(default)]` on fields the C++ `to_json` emits +//! unconditionally (all of them, here). +//! 3. A round-trip test (`test_runtime_config_schema_stability`) pins the +//! v0.20 shape so silent drift is impossible. + use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct ConcurrencyRuntimeConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub update_merkle_tree: Option, +// --------------------------------------------------------------------------- +// Console configuration +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::console_output_destination`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ConsoleOutputDestination { + ToNull, + ToStdout, + ToStderr, + ToFd, + ToFile, + ToBuffer, +} + +impl Default for ConsoleOutputDestination { + /// Matches the C++ in-struct initializer (`console_output_destination::to_stdout`). + fn default() -> Self { + Self::ToStdout + } +} + +/// Mirror of C++ `cartesi::console_flush_mode`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ConsoleFlushMode { + WhenFull, + EveryChar, + EveryLine, +} + +impl Default for ConsoleFlushMode { + /// Matches the C++ in-struct initializer (`console_flush_mode::every_line`). + fn default() -> Self { + Self::EveryLine + } +} + +/// Mirror of C++ `cartesi::console_input_source`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ConsoleInputSource { + FromNull, + FromStdin, + FromFd, + FromFile, + FromBuffer, +} + +impl Default for ConsoleInputSource { + /// Matches the C++ in-struct initializer (`console_input_source::from_null`). + fn default() -> Self { + Self::FromNull + } +} + +/// Mirror of C++ `cartesi::console_runtime_config`. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ConsoleRuntimeConfig { + pub output_destination: ConsoleOutputDestination, + pub output_flush_mode: ConsoleFlushMode, + pub output_buffer_size: u64, + pub output_fd: i32, + pub output_filename: String, + + pub input_source: ConsoleInputSource, + pub input_buffer_size: u64, + pub input_fd: i32, + pub input_filename: String, + + pub tty_cols: u16, + pub tty_rows: u16, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct HTIFRuntimeConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub no_console_putchar: Option, +impl Default for ConsoleRuntimeConfig { + /// Matches the in-struct initializers in `machine-runtime-config.h` and the + /// `os::TTY_DEFAULT_*` constants from v0.20 (`os.h`: cols=80, rows=25). + fn default() -> Self { + Self { + output_destination: ConsoleOutputDestination::default(), + output_flush_mode: ConsoleFlushMode::default(), + output_buffer_size: 4096, + output_fd: -1, + output_filename: String::new(), + + input_source: ConsoleInputSource::default(), + input_buffer_size: 4096, + input_fd: -1, + input_filename: String::new(), + + tty_cols: 80, + tty_rows: 25, + } + } } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] +// --------------------------------------------------------------------------- +// Concurrency and top-level runtime configuration +// --------------------------------------------------------------------------- + +/// Mirror of C++ `cartesi::concurrency_runtime_config`. Note: v0.19's +/// `update_merkle_tree` field was renamed to `update_hash_tree` in v0.20. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ConcurrencyRuntimeConfig { + pub update_hash_tree: u64, +} + +/// Mirror of C++ `cartesi::machine_runtime_config`. +/// +/// The v0.19 binding had top-level `htif`, `skip_root_hash_check`, and +/// `skip_root_hash_store` fields. None of those exist on the v0.20 +/// `machine_runtime_config`. The equivalent of v0.19's +/// `htif.no_console_putchar` is now `console.output_destination = ToNull`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] pub struct RuntimeConfig { - #[serde(skip_serializing_if = "Option::is_none")] - pub concurrency: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub htif: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub skip_root_hash_check: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub skip_root_hash_store: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub skip_version_check: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub soft_yield: Option, + pub console: ConsoleRuntimeConfig, + pub concurrency: ConcurrencyRuntimeConfig, + pub skip_version_check: bool, + pub soft_yield: bool, + pub no_reserve: bool, +} + +impl RuntimeConfig { + /// Convenience for "run the machine without touching the host console" — + /// replaces the v0.19 pattern of setting `htif.no_console_putchar = true`. + pub fn quiet_console() -> Self { + Self { + console: ConsoleRuntimeConfig { + output_destination: ConsoleOutputDestination::ToNull, + input_source: ConsoleInputSource::FromNull, + ..ConsoleRuntimeConfig::default() + }, + ..Self::default() + } + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + /// Static schema-completeness test: pins the v0.20 `machine_runtime_config` + /// JSON shape directly against `RuntimeConfig`, so drift in either + /// direction surfaces as a test failure. + /// + /// The JSON below is constructed from `src/json-util.cpp::to_json + /// (machine_runtime_config)` and the default values in + /// `machine-runtime-config.h` / `os.h` (TTY_DEFAULT_COLS=80, + /// TTY_DEFAULT_ROWS=25). + #[test] + fn test_runtime_config_schema_stability() { + let v020_json = serde_json::json!({ + "console": { + "output_destination": "to_stdout", + "output_flush_mode": "every_line", + "output_buffer_size": 4096u64, + "output_fd": -1, + "output_filename": "", + "input_source": "from_null", + "input_buffer_size": 4096u64, + "input_fd": -1, + "input_filename": "", + "tty_cols": 80, + "tty_rows": 25, + }, + "concurrency": { "update_hash_tree": 0u64 }, + "skip_version_check": false, + "soft_yield": false, + "no_reserve": false, + }); + + let typed: RuntimeConfig = serde_json::from_value(v020_json.clone()) + .expect("v0.20 runtime JSON should parse into RuntimeConfig"); + let reserialized = serde_json::to_value(&typed).expect("re-serialization failed"); + + assert_eq!( + v020_json, reserialized, + "RuntimeConfig round-trip lost or added data. Schema drift vs the C++ side." + ); + } + + #[test] + fn test_runtime_config_default_round_trips() { + let cfg = RuntimeConfig::default(); + let json = serde_json::to_value(&cfg).expect("serialization should succeed"); + let back: RuntimeConfig = + serde_json::from_value(json).expect("deserialization should succeed"); + assert_eq!(cfg, back); + } + + #[test] + fn test_runtime_config_quiet_console() { + let cfg = RuntimeConfig::quiet_console(); + assert_eq!( + cfg.console.output_destination, + ConsoleOutputDestination::ToNull + ); + assert_eq!(cfg.console.input_source, ConsoleInputSource::FromNull); + } + + #[test] + fn test_runtime_config_unknown_field_rejected() { + let json = serde_json::json!({ + "console": { + "output_destination": "to_stdout", + "output_flush_mode": "every_line", + "output_buffer_size": 4096u64, + "output_fd": -1, + "output_filename": "", + "input_source": "from_null", + "input_buffer_size": 4096u64, + "input_fd": -1, + "input_filename": "", + "tty_cols": 80, + "tty_rows": 25, + }, + "concurrency": { "update_hash_tree": 0u64 }, + "skip_version_check": false, + "soft_yield": false, + "no_reserve": false, + "something_new": true, + }); + assert!( + serde_json::from_value::(json).is_err(), + "deny_unknown_fields must reject previously-unseen top-level keys" + ); + } } diff --git a/machine/rust-bindings/cartesi-machine/src/constants.rs b/machine/rust-bindings/cartesi-machine/src/constants.rs index fef0a375..92403e41 100644 --- a/machine/rust-bindings/cartesi-machine/src/constants.rs +++ b/machine/rust-bindings/cartesi-machine/src/constants.rs @@ -1,24 +1,39 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -//! Constants definitions from Cartesi Machine +//! Constants definitions from Cartesi Machine. +//! +//! The names in this module track the v0.20 emulator naming convention: +//! `HASH_TREE_LOG2_*` for hash-tree sizes (previously `TREE_LOG2_*`) and the +//! `ar` module for address ranges (previously `pma`). The numeric values are +//! unchanged from v0.19. pub mod machine { use cartesi_machine_sys::*; // pub const CYCLE_MAX: u64 = CM_MCYCLE_MAX as u64; pub const HASH_SIZE: u32 = CM_HASH_SIZE as u32; - pub const TREE_LOG2_WORD_SIZE: u32 = CM_HASH_TREE_LOG2_WORD_SIZE as u32; - pub const TREE_LOG2_PAGE_SIZE: u32 = CM_HASH_TREE_LOG2_PAGE_SIZE as u32; - pub const TREE_LOG2_ROOT_SIZE: u32 = CM_HASH_TREE_LOG2_ROOT_SIZE as u32; + pub const HASH_TREE_LOG2_WORD_SIZE: u32 = CM_HASH_TREE_LOG2_WORD_SIZE as u32; + pub const HASH_TREE_LOG2_PAGE_SIZE: u32 = CM_HASH_TREE_LOG2_PAGE_SIZE as u32; + pub const HASH_TREE_LOG2_ROOT_SIZE: u32 = CM_HASH_TREE_LOG2_ROOT_SIZE as u32; } -pub mod pma { +pub mod ar { use cartesi_machine_sys::*; pub const RX_START: u64 = CM_AR_CMIO_RX_BUFFER_START as u64; pub const RX_LOG2_SIZE: u64 = CM_AR_CMIO_RX_BUFFER_LOG2_SIZE as u64; pub const TX_START: u64 = CM_AR_CMIO_TX_BUFFER_START as u64; pub const TX_LOG2_SIZE: u64 = CM_AR_CMIO_TX_BUFFER_LOG2_SIZE as u64; pub const RAM_START: u64 = CM_AR_RAM_START as u64; + /// Dedicated memory slot the off-chain client writes the pre-input root + /// hash to before sending a CMIO input, so that on-chain + /// `revertIfNeeded` can read it back after a rejected input. + /// + /// Canonical source is the emulator C++; the Solidity side mirrors it + /// through the auto-generated + /// `step/src/EmulatorConstants.sol::REVERT_ROOT_HASH_ADDRESS`, used + /// only from `EmulatorCompat.{set,get}RevertRootHash` wrappers — no + /// other Solidity file should reference the raw address. + pub const SHADOW_REVERT_ROOT_HASH_START: u64 = CM_AR_SHADOW_REVERT_ROOT_HASH_START as u64; } pub mod break_reason { diff --git a/machine/rust-bindings/cartesi-machine/src/lib.rs b/machine/rust-bindings/cartesi-machine/src/lib.rs index 06c0222a..b5b4cb62 100644 --- a/machine/rust-bindings/cartesi-machine/src/lib.rs +++ b/machine/rust-bindings/cartesi-machine/src/lib.rs @@ -13,3 +13,22 @@ pub use machine::Machine; // Reexport inner cartesi-machine-sys pub use cartesi_machine_sys; + +/// Emulator semantic version these bindings were written against, encoded per +/// the convention from `machine-c-api.h`: +/// `(major * 1000000) + (minor * 1000) + patch`. +/// +/// The `test_emulator_version_pin` test asserts at build time that the linked +/// `libcartesi` reports this exact version. Bumping the emulator requires +/// bumping this constant and re-running the config round-trip tests — any +/// schema drift will surface there. +pub const EXPECTED_EMULATOR_VERSION: u64 = 20_000; // 0.20.0 + +/// Formats an emulator version u64 (as returned by `cm_get_version`) as +/// `"major.minor.patch"`. +pub fn format_emulator_version(v: u64) -> String { + let major = v / 1_000_000; + let minor = (v / 1_000) % 1_000; + let patch = v % 1_000; + format!("{major}.{minor}.{patch}") +} diff --git a/machine/rust-bindings/cartesi-machine/src/machine.rs b/machine/rust-bindings/cartesi-machine/src/machine.rs index 2bc75553..4e45f9f6 100644 --- a/machine/rust-bindings/cartesi-machine/src/machine.rs +++ b/machine/rust-bindings/cartesi-machine/src/machine.rs @@ -20,15 +20,28 @@ use crate::{ }, }; -/// Machine instance handle +/// Machine instance handle. +/// +/// Owns a `*mut cm_machine` and frees it on `Drop` via `cm_delete`. The raw +/// pointer is kept private — exposing it would let callers clone it and cause +/// a double-free when both `Machine`s get dropped. +/// +/// `Machine` is intentionally `!Send + !Sync` (the default, given the raw +/// pointer field). Do not add `unsafe impl Send for Machine` without auditing +/// `cm_get_last_error_message`: the C library threads error messages through +/// thread-local (or global) state with no machine-instance argument, so two +/// `Machine`s running on different threads could scramble each other's +/// `MachineError::message` fields. pub struct Machine { - pub machine: *mut cartesi_machine_sys::cm_machine, + machine: *mut cartesi_machine_sys::cm_machine, } impl Drop for Machine { fn drop(&mut self) { - unsafe { - cartesi_machine_sys::cm_delete(self.machine); + if !self.machine.is_null() { + unsafe { + cartesi_machine_sys::cm_delete(self.machine); + } } } } @@ -43,6 +56,16 @@ macro_rules! check_err { }; } +/// Both `serde_json::to_string` and `CString::new` below panic on failure +/// *by design*. They can only fail on: +/// - A Rust config type holding non-serializable state. Our types are plain +/// POD with derived `Serialize`, so this is statically impossible. +/// - A JSON string containing an interior NUL byte. `serde_json` escapes NUL +/// as `\u0000`, so the output is guaranteed NUL-free. +/// +/// If either panic ever fires, it indicates a bug in this crate or in +/// `serde_json` — there is no recovery, and a panic with a backtrace is more +/// debuggable than a bubbled-up `Result` that crashes at the caller anyway. macro_rules! serialize_to_json { ($src:expr) => { CString::new(serde_json::to_string($src).expect("failed serializing to json")) @@ -50,6 +73,13 @@ macro_rules! serialize_to_json { }; } +/// Panics on malformed JSON from the C library. This means either a bug in +/// `libcartesi` or a mismatch between the Rust struct shape and the +/// emulator's JSON schema — the round-trip tests in `config/machine.rs` and +/// `config/runtime.rs` are expected to catch the latter before production. +/// A `Result` return here would force every call site to propagate an error +/// variant for a condition with no meaningful recovery path; panicking gives +/// a clearer stacktrace. macro_rules! parse_json_from_cstring { ($src:expr) => {{ let cstr = unsafe { CStr::from_ptr($src) }; @@ -63,16 +93,31 @@ impl Machine { // API functions // ----------------------------------------------------------------------------- - /// Returns the default machine config. + /// Returns the emulator semantic version of the linked `libcartesi`, as + /// returned by `cm_get_version`. Encoded as + /// `(major * 1000000) + (minor * 1000) + patch`. + pub fn version() -> u64 { + unsafe { cartesi_machine_sys::cm_get_version() } + } + + /// Returns the default machine config as parsed by serde. pub fn default_config() -> Result { + let raw = Self::default_config_raw_json()?; + Ok(serde_json::from_str(&raw) + .expect("cm_get_default_config returned JSON that does not match MachineConfig")) + } + + /// Returns the raw JSON string produced by `cm_get_default_config`, + /// without deserializing into a typed struct. Primarily used by the + /// round-trip schema test. + pub fn default_config_raw_json() -> Result { let mut config_ptr: *const c_char = ptr::null(); let err_code = unsafe { cartesi_machine_sys::cm_get_default_config(ptr::null(), &mut config_ptr) }; check_err!(err_code)?; - let config = parse_json_from_cstring!(config_ptr); - - Ok(config) + let cstr = unsafe { CStr::from_ptr(config_ptr) }; + Ok(cstr.to_string_lossy().into_owned()) } /// Gets the address of any x, f, or control state register. @@ -110,7 +155,7 @@ impl Machine { /// Loads a new machine instance from a previously stored directory. pub fn load(dir: &Path, runtime_config: &RuntimeConfig) -> Result { - let dir_cstr = path_to_cstring(dir); + let dir_cstr = path_to_cstring(dir)?; let runtime_config_json = serialize_to_json!(&runtime_config); let mut machine: *mut cartesi_machine_sys::cm_machine = ptr::null_mut(); @@ -132,7 +177,7 @@ impl Machine { /// address ranges (required when storing in-memory machines that have no /// backing files). pub fn store(&mut self, dir: &Path) -> Result<()> { - let dir_cstr = path_to_cstring(dir); + let dir_cstr = path_to_cstring(dir)?; let err_code = unsafe { cartesi_machine_sys::cm_store( self.machine, @@ -156,19 +201,42 @@ impl Machine { Ok(()) } - /// Gets the machine runtime config. + /// Gets the machine runtime config as parsed by serde. pub fn runtime_config(&mut self) -> Result { + let raw = self.runtime_config_raw_json()?; + Ok(serde_json::from_str(&raw) + .expect("cm_get_runtime_config returned JSON that does not match RuntimeConfig")) + } + + /// Returns the raw JSON string produced by `cm_get_runtime_config`. Used + /// by the round-trip schema test. + pub fn runtime_config_raw_json(&mut self) -> Result { let mut rc_ptr: *const c_char = ptr::null(); let err_code = unsafe { cartesi_machine_sys::cm_get_runtime_config(self.machine, &mut rc_ptr) }; check_err!(err_code)?; - let runtime_config = parse_json_from_cstring!(rc_ptr); - - Ok(runtime_config) + let cstr = unsafe { CStr::from_ptr(rc_ptr) }; + Ok(cstr.to_string_lossy().into_owned()) } /// Replaces a memory range. + /// + /// Two intentional simplifications vs. the full JSON schema the C API + /// accepts: + /// + /// - `read_only` is hardcoded to `false`. The C++ + /// `machine_address_ranges::replace` explicitly rejects both a + /// read-only existing range and a replacement config with + /// `read_only: true` (see `machine-address-ranges.cpp`), so exposing a + /// `read_only` toggle here would always error. If that ever changes, + /// widen this API then. + /// - When `image_path` is `None`, `data_filename` is serialized as the + /// empty string. The C++ side treats empty `data_filename` as "no + /// backing store" (`backing_store_config::newly_created()` returns + /// true when `create || data_filename.empty()`), which is the same + /// semantics as the old API's `NULL` pointer: the range is + /// zero-filled in-memory. pub fn replace_memory_range( &mut self, start: u64, @@ -241,7 +309,7 @@ impl Machine { cartesi_machine_sys::cm_get_proof( self.machine, address, - log2_target_size as i32, + log2_target_size as ::std::os::raw::c_int, log2_root_size as ::std::os::raw::c_int, &mut proof_ptr, ) @@ -286,7 +354,7 @@ impl Machine { /// Reads a chunk of data from a machine memory range, by its physical address. pub fn read_memory(&mut self, address: u64, size: u64) -> Result> { - let mut buffer = vec![0u8; size as usize]; + let mut buffer = vec![0u8; u64_to_usize(size)?]; let err_code = unsafe { cartesi_machine_sys::cm_read_memory(self.machine, address, buffer.as_mut_ptr(), size) }; @@ -312,7 +380,7 @@ impl Machine { /// Reads a chunk of data from a machine memory range, by its virtual memory. pub fn read_virtual_memory(&mut self, address: u64, size: u64) -> Result> { - let mut buffer = vec![0u8; size as usize]; + let mut buffer = vec![0u8; u64_to_usize(size)?]; let err_code = unsafe { cartesi_machine_sys::cm_read_virtual_memory( self.machine, @@ -420,7 +488,10 @@ impl Machine { let mut reason: u16 = 0; let mut length: u64 = 0; - // if data is NULL, length will still be set without reading any data. + // First call with a NULL data pointer: the C API just writes the + // required length into `length` and returns, without reading any + // bytes. (See machine-c-api.h: "If NULL, length will still be set + // without reading any data.") let err_code = unsafe { cartesi_machine_sys::cm_receive_cmio_request( self.machine, @@ -432,7 +503,11 @@ impl Machine { }; check_err!(err_code)?; - let mut buffer = vec![0u8; length as usize]; + // `length` is in-out per the C API contract ("Must be initialized to + // the size of data buffer"). Sizing the buffer to exactly `length` + // and then passing the same value back in makes the buffer-size + // precondition and the required-length output coincide. + let mut buffer = vec![0u8; u64_to_usize(length)?]; let err_code = unsafe { cartesi_machine_sys::cm_receive_cmio_request( @@ -470,7 +545,7 @@ impl Machine { /// Runs the machine for the given mcycle count and generates a log of accessed pages and proof data. pub fn log_step(&mut self, mcycle_count: u64, log_filename: &Path) -> Result { let mut break_reason = BreakReason::default(); - let log_filename_c = path_to_cstring(log_filename); + let log_filename_c = path_to_cstring(log_filename)?; let err_code = unsafe { cartesi_machine_sys::cm_log_step( @@ -555,7 +630,7 @@ impl Machine { mcycle_count: u64, root_hash_after: &Hash, ) -> Result { - let log_filename_c = path_to_cstring(log_filename); + let log_filename_c = path_to_cstring(log_filename)?; let mut break_reason = BreakReason::default(); let err_code = unsafe { @@ -582,6 +657,9 @@ impl Machine { let err_code = unsafe { cartesi_machine_sys::cm_verify_step_uarch( + // Optional `const cm_machine *m`; NULL means "local verification". + // See machine-c-api.h. (cm_verify_step itself doesn't take this + // argument — the asymmetry is intentional in the C API.) ptr::null(), root_hash_before, log_cstr.as_ptr(), @@ -602,6 +680,8 @@ impl Machine { let log_cstr = serialize_to_json!(&log); let err_code = unsafe { cartesi_machine_sys::cm_verify_reset_uarch( + // Optional `const cm_machine *m`; NULL means "local verification". + // See machine-c-api.h. ptr::null(), root_hash_before, log_cstr.as_ptr(), @@ -625,6 +705,8 @@ impl Machine { let err_code = unsafe { cartesi_machine_sys::cm_verify_send_cmio_response( + // Optional `const cm_machine *m`; NULL means "local verification". + // See machine-c-api.h. ptr::null(), reason as u16, data.as_ptr(), @@ -652,8 +734,49 @@ impl Machine { } } -fn path_to_cstring(path: &Path) -> CString { - CString::new(path.to_string_lossy().as_bytes()).expect("CString::new failed") +/// Converts a `u64` byte count (as used by the C API) to a Rust `usize`, +/// erroring out if the value exceeds what the platform can address. Only +/// matters on 32-bit targets — on 64-bit, `usize` and `u64` are the same +/// width and this is a no-op. Guards against silent truncation that would +/// result in an undersized buffer being passed to a C function expecting +/// `size` bytes of space. +fn u64_to_usize(size: u64) -> Result { + usize::try_from(size).map_err(|_| MachineError { + code: constants::error_code::OUT_OF_RANGE, + message: format!("byte count {size} exceeds usize range on this platform"), + }) +} + +/// Converts a `Path` to a `CString` for the C API. +/// +/// On Unix, uses the raw `OsStr` bytes so that non-UTF-8 paths (which are +/// legal on the platform) are passed through verbatim instead of being +/// silently corrupted by `to_string_lossy` replacement. On other platforms, +/// falls back to UTF-8 conversion and errors out if the path is not valid +/// UTF-8. +/// +/// Returns `CM_ERROR_INVALID_ARGUMENT` on an interior NUL byte or, on +/// non-Unix, on a non-UTF-8 path. +fn path_to_cstring(path: &Path) -> Result { + #[cfg(unix)] + let bytes = { + use std::os::unix::ffi::OsStrExt; + path.as_os_str().as_bytes().to_vec() + }; + #[cfg(not(unix))] + let bytes = path + .to_str() + .ok_or_else(|| MachineError { + code: constants::error_code::INVALID_ARGUMENT, + message: format!("path is not valid UTF-8: {}", path.display()), + })? + .as_bytes() + .to_vec(); + + CString::new(bytes).map_err(|e| MachineError { + code: constants::error_code::INVALID_ARGUMENT, + message: format!("path contains NUL byte ({}): {}", e, path.display()), + }) } #[cfg(test)] @@ -714,13 +837,7 @@ mod tests { } fn create_machine(config: &MachineConfig) -> Result { - let runtime_config = RuntimeConfig { - htif: Some(config::runtime::HTIFRuntimeConfig { - no_console_putchar: Some(true), - }), - ..Default::default() - }; - Machine::create(config, &runtime_config) + Machine::create(config, &RuntimeConfig::quiet_console()) } #[test] @@ -943,7 +1060,7 @@ mod tests { let proof: Proof = machine.proof( range.start, u64::BITS - range.length.leading_zeros(), - constants::machine::TREE_LOG2_ROOT_SIZE, + constants::machine::HASH_TREE_LOG2_ROOT_SIZE, )?; assert_eq!(proof.target_address, range.start); assert_eq!(proof.log2_target_size, log2_size as u64); diff --git a/prt/client-lua/computation/constants.lua b/prt/client-lua/computation/constants.lua index add83f6a..5bcb878a 100644 --- a/prt/client-lua/computation/constants.lua +++ b/prt/client-lua/computation/constants.lua @@ -1,4 +1,5 @@ local arithmetic = require "utils.arithmetic" +local cartesi = require "cartesi" -- log2 value of the maximal number of micro instructions that emulates a big instruction local log2_uarch_span_to_barch = 20 @@ -11,8 +12,14 @@ local log2_uarch_span_to_input = log2_uarch_span_to_barch + log2_barch_span_to_i -- log2 value of the maximal number of meta instructions local log2_uarch_span_to_epoch = log2_input_span_to_epoch + log2_barch_span_to_input + log2_uarch_span_to_barch --- Checkpoint address for machine state snapshots -local CHECKPOINT_ADDRESS = 0xfe0 +-- Memory slot where the off-chain client writes the pre-input root hash +-- before sending a CMIO input, so that on-chain `revertIfNeeded` can read it +-- back after a rejected input. Sourced from the emulator directly (v0.20+ +-- `cartesi.AR_SHADOW_REVERT_ROOT_HASH_START`, currently 0xfe0); the Solidity +-- side mirrors this through step's auto-generated +-- `EmulatorConstants.REVERT_ROOT_HASH_ADDRESS`. +local CHECKPOINT_ADDRESS = cartesi.AR_SHADOW_REVERT_ROOT_HASH_START +assert(CHECKPOINT_ADDRESS, "emulator missing AR_SHADOW_REVERT_ROOT_HASH_START (expected v0.20+)") local constants = { log2_uarch_span_to_barch = log2_uarch_span_to_barch, diff --git a/prt/client-rs/core/src/machine/constants.rs b/prt/client-rs/core/src/machine/constants.rs index 97f6654e..6bc96821 100644 --- a/prt/client-rs/core/src/machine/constants.rs +++ b/prt/client-rs/core/src/machine/constants.rs @@ -14,3 +14,64 @@ pub const INPUT_SPAN_TO_EPOCH: u64 = arithmetic::max_uint(LOG2_INPUT_SPAN_TO_EPO // log2 value of the maximal number of micro instructions that executes an input pub const LOG2_UARCH_SPAN_TO_INPUT: u64 = LOG2_BARCH_SPAN_TO_INPUT + LOG2_UARCH_SPAN_TO_BARCH; + +/// Re-export of the emulator's dedicated memory slot for the pre-input root +/// hash (a.k.a. `CM_AR_SHADOW_REVERT_ROOT_HASH_START`, currently `0xfe0`). +/// +/// The off-chain client writes the current root hash to this address before +/// sending a CMIO input, so that on-chain `revertIfNeeded` can read it back +/// and restore the state after a rejected input. The Solidity side mirrors +/// the emulator through step's auto-generated +/// `EmulatorConstants.REVERT_ROOT_HASH_ADDRESS`; +/// `tests::test_emulator_and_step_agree_on_revert_address` asserts the two +/// stay in sync after any emulator or step bump. +pub use cartesi_machine::constants::ar::SHADOW_REVERT_ROOT_HASH_START as CHECKPOINT_ADDRESS; + +#[cfg(test)] +mod tests { + use super::CHECKPOINT_ADDRESS; + + /// Guardrail: step's `EmulatorConstants.sol` is auto-generated from the + /// emulator C++ source, and `REVERT_ROOT_HASH_ADDRESS` must equal the + /// emulator's `CM_AR_SHADOW_REVERT_ROOT_HASH_START` — otherwise the + /// off-chain client writes to one address while on-chain + /// `revertIfNeeded` reads from another, and any rejected-input dispute + /// mis-restores state. If this test fails after an emulator or step + /// bump, the step submodule is out of sync with the emulator version + /// these bindings link against: regenerate step's `EmulatorConstants.sol` + /// against the matching emulator and bump both submodule pointers + /// together. + #[test] + fn test_emulator_and_step_agree_on_revert_address() { + let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let emulator_constants_sol = manifest_dir + .join("../../..") + .join("machine/step/src/EmulatorConstants.sol"); + let source = std::fs::read_to_string(&emulator_constants_sol) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", emulator_constants_sol.display())); + + // Find: `uint64 constant REVERT_ROOT_HASH_ADDRESS = 0x;` + let marker = "REVERT_ROOT_HASH_ADDRESS"; + let pos = source.find(marker).unwrap_or_else(|| { + panic!("{marker} not found in {}", emulator_constants_sol.display()) + }); + let after = &source[pos + marker.len()..]; + let eq = after.find('=').expect("expected `=` after constant name"); + let semi = after.find(';').expect("expected `;` after constant value"); + let value_str = after[eq + 1..semi].trim(); + let step_value = if let Some(hex) = value_str.strip_prefix("0x") { + u64::from_str_radix(hex, 16).expect("REVERT_ROOT_HASH_ADDRESS not valid hex") + } else { + value_str + .parse::() + .expect("REVERT_ROOT_HASH_ADDRESS not valid decimal") + }; + + assert_eq!( + CHECKPOINT_ADDRESS, step_value, + "Emulator CM_AR_SHADOW_REVERT_ROOT_HASH_START ({CHECKPOINT_ADDRESS:#x}) \ + does not match step's EmulatorConstants.REVERT_ROOT_HASH_ADDRESS ({step_value:#x}). \ + The off-chain client and on-chain verifier will disagree on the revert slot." + ); + } +} diff --git a/prt/client-rs/core/src/machine/instance.rs b/prt/client-rs/core/src/machine/instance.rs index 71b4b5b5..13fe5719 100644 --- a/prt/client-rs/core/src/machine/instance.rs +++ b/prt/client-rs/core/src/machine/instance.rs @@ -1,15 +1,15 @@ use crate::db::dispute_state_access::DisputeStateAccess; use crate::machine::constants::{ - BARCH_SPAN_TO_INPUT, INPUT_SPAN_TO_EPOCH, LOG2_UARCH_SPAN_TO_BARCH, LOG2_UARCH_SPAN_TO_INPUT, - UARCH_SPAN_TO_BARCH, + BARCH_SPAN_TO_INPUT, CHECKPOINT_ADDRESS, INPUT_SPAN_TO_EPOCH, LOG2_UARCH_SPAN_TO_BARCH, + LOG2_UARCH_SPAN_TO_INPUT, UARCH_SPAN_TO_BARCH, }; use crate::machine::error::Result; use cartesi_dave_arithmetic as arithmetic; use cartesi_dave_merkle::Digest; use cartesi_machine::{ cartesi_machine_sys, - config::runtime::{HTIFRuntimeConfig, RuntimeConfig}, - constants::machine::TREE_LOG2_ROOT_SIZE, + config::runtime::RuntimeConfig, + constants::machine::HASH_TREE_LOG2_ROOT_SIZE, machine::Machine, types::access_proof::AccessLog, types::{LogType, cmio::CmioResponseReason}, @@ -64,15 +64,9 @@ pub struct MachineInstance { pub snapshot_path: PathBuf, } -const CHECKPOINT_ADDRESS: u64 = 0xfe0; impl MachineInstance { pub fn new_from_path(path: &str) -> Result { - let runtime_config = RuntimeConfig { - htif: Some(HTIFRuntimeConfig { - no_console_putchar: Some(true), - }), - ..Default::default() - }; + let runtime_config = RuntimeConfig::quiet_console(); let path = PathBuf::from(path); let mut machine = Machine::load(&path, &runtime_config)?; @@ -110,12 +104,7 @@ impl MachineInstance { // load inner machine with snapshot, update cycle, keep everything else the same pub fn load_snapshot(&mut self, snapshot_path: &Path, snapshot_cycle: u64) -> Result<()> { debug!("load snapshot from {}", snapshot_path.display()); - let runtime_config = RuntimeConfig { - htif: Some(HTIFRuntimeConfig { - no_console_putchar: Some(true), - }), - ..Default::default() - }; + let runtime_config = RuntimeConfig::quiet_console(); let mut machine = Machine::load(Path::new(snapshot_path), &runtime_config)?; let cycle = machine.mcycle()?; @@ -257,12 +246,7 @@ impl MachineInstance { != cartesi_machine::constants::cmio::tohost::manual::RX_ACCEPTED { trace!("Reject input,revert to previous snapshot"); - let runtime_config = RuntimeConfig { - htif: Some(HTIFRuntimeConfig { - no_console_putchar: Some(true), - }), - ..Default::default() - }; + let runtime_config = RuntimeConfig::quiet_console(); self.machine = Machine::load(&self.snapshot_path, &runtime_config)?; } @@ -335,7 +319,7 @@ impl MachineInstance { let mut read = self.machine.read_memory(aligned_address, 32)?; let proof = self .machine - .proof(aligned_address, 5, TREE_LOG2_ROOT_SIZE)?; + .proof(aligned_address, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let mut encoded: Vec = Vec::new(); @@ -355,7 +339,7 @@ impl MachineInstance { let read_hash = Digest::from_data(&read); let proof = self .machine - .proof(aligned_address, 5, TREE_LOG2_ROOT_SIZE)?; + .proof(aligned_address, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let mut encoded: Vec = Vec::new(); @@ -375,7 +359,7 @@ impl MachineInstance { let read = self.machine.read_memory(address, 32)?; let read_hash = Digest::from_data(&read); // Get proof of write address - let proof = self.machine.proof(address, 5, TREE_LOG2_ROOT_SIZE)?; + let proof = self.machine.proof(address, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let mut encoded: Vec = Vec::new(); diff --git a/prt/tests/rollups/justfile b/prt/tests/rollups/justfile index 5aa45d93..d58390e9 100644 --- a/prt/tests/rollups/justfile +++ b/prt/tests/rollups/justfile @@ -10,7 +10,7 @@ test PROGRAM SCRIPT: ANVIL_LOAD_PATH=`realpath {{ANVIL_LOAD_PATH}}` \ ANVIL_DUMP_PATH="anvil_{{PROGRAM}}_{{SCRIPT}}.json" \ TEMPLATE_MACHINE=`realpath ../../../test/programs/{{PROGRAM}}/machine-image` \ - TEMPLATE_MACHINE_HASH=0x`xxd -seek 0x60 -l 0x20 -c 0x20 -p ../../../test/programs/{{PROGRAM}}/machine-image/hash_tree.sht` \ + TEMPLATE_MACHINE_HASH=0x`cartesi-machine-stored-hash ../../../test/programs/{{PROGRAM}}/machine-image` \ DAVE_APP_FACTORY=`jq -r .address {{DEPLOYMENTS_DIR}}/DaveAppFactory.json` \ INPUT_BOX=`jq -r .address {{DEPLOYMENTS_DIR}}/InputBox.json` \ ERC20_PORTAL=`jq -r .address {{DEPLOYMENTS_DIR}}/ERC20Portal.json` \