diff --git a/.github/actions/cartesi-machine/action.yml b/.github/actions/cartesi-machine/action.yml index 8df8a10c..bc40b93b 100644 --- a/.github/actions/cartesi-machine/action.yml +++ b/.github/actions/cartesi-machine/action.yml @@ -4,7 +4,7 @@ inputs: version: description: 'Version of Cartesi Machine to install' required: false - default: 0.19.0 + default: 0.20.0 suffix-version: description: 'Suffix of Cartesi Machine to install' required: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d879e8fd..ebe9a738 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: - name: Install Cartesi Machine uses: ./.github/actions/cartesi-machine with: - version: 0.19.0 + version: 0.20.0 suffix-version: "" - name: Setup env @@ -71,7 +71,7 @@ jobs: - name: Install Cartesi Machine uses: ./.github/actions/cartesi-machine with: - version: 0.19.0 + version: 0.20.0 - name: Download PRT contracts working-directory: ./prt/contracts run: | @@ -138,8 +138,8 @@ jobs: working-directory: ./machine/emulator run: | make bundle-boost - wget -O add-generated-files.diff https://github.com/cartesi/machine-emulator/releases/download/v0.19.0/add-generated-files.diff - echo "a892e2d9f5c331f5e80bcb5db4133e7db625aa4d14ffdf9467b75c4c34d1744f add-generated-files.diff" | sha256sum -c + wget -O add-generated-files.diff https://github.com/cartesi/machine-emulator/releases/download/v0.20.0/add-generated-files.diff + echo "d9c2afcefc2759e7cd37bbedc83d54c81515f0fddb671103b489b8789aee33bb add-generated-files.diff" | sha256sum -c git apply add-generated-files.diff rm add-generated-files.diff make diff --git a/cartesi-rollups/contracts/src/DaveConsensus.sol b/cartesi-rollups/contracts/src/DaveConsensus.sol index eabf87f7..bde7f1ad 100644 --- a/cartesi-rollups/contracts/src/DaveConsensus.sol +++ b/cartesi-rollups/contracts/src/DaveConsensus.sol @@ -242,7 +242,7 @@ contract DaveConsensus is IDaveConsensus, ERC165, ApplicationChecker { require(proof.length == Memory.LOG2_MAX_SIZE, InvalidOutputsMerkleRootProofSize(proof.length)); bytes32 allegedStateHash = proof.merkleRootAfterReplacement( - EmulatorConstants.PMA_CMIO_TX_BUFFER_START >> EmulatorConstants.TREE_LOG2_WORD_SIZE, + EmulatorConstants.AR_CMIO_TX_BUFFER_START >> EmulatorConstants.HASH_TREE_LOG2_WORD_SIZE, keccak256(abi.encode(outputsMerkleRoot)), LibKeccak256.hashPair ); diff --git a/cartesi-rollups/contracts/test/DaveAppFactory.t.sol b/cartesi-rollups/contracts/test/DaveAppFactory.t.sol index ff954c41..b67b71fa 100644 --- a/cartesi-rollups/contracts/test/DaveAppFactory.t.sol +++ b/cartesi-rollups/contracts/test/DaveAppFactory.t.sol @@ -167,7 +167,7 @@ contract DaveAppFactoryTest is Test { bytes32[] memory outputsMerkleRootProof = _randomProof(Memory.LOG2_MAX_SIZE); bytes32 machineMerkleRoot = outputsMerkleRootProof.merkleRootAfterReplacement( - EmulatorConstants.PMA_CMIO_TX_BUFFER_START >> EmulatorConstants.TREE_LOG2_WORD_SIZE, + EmulatorConstants.AR_CMIO_TX_BUFFER_START >> EmulatorConstants.HASH_TREE_LOG2_WORD_SIZE, keccak256(abi.encode(outputsMerkleRoot)) ); diff --git a/cartesi-rollups/node/blockchain-reader/src/lib.rs b/cartesi-rollups/node/blockchain-reader/src/lib.rs index ce95bf1c..e86472bd 100644 --- a/cartesi-rollups/node/blockchain-reader/src/lib.rs +++ b/cartesi-rollups/node/blockchain-reader/src/lib.rs @@ -572,7 +572,10 @@ mod blockchain_reader_tests { let mut machine = Machine::create( &MachineConfig::new_with_ram(RAMConfig { length: 134217728, - image_filename: "../../../test/programs/linux.bin".into(), + backing_store: cartesi_machine::config::machine::BackingStoreConfig { + data_filename: "../../../test/programs/linux.bin".into(), + ..Default::default() + }, }), &RuntimeConfig::default(), ) diff --git a/cartesi-rollups/node/blockchain-reader/src/test_utils.rs b/cartesi-rollups/node/blockchain-reader/src/test_utils.rs index fc9c49ce..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, - path::PathBuf, -}; +use std::{fs, path::PathBuf}; type Result = std::result::Result>; @@ -76,12 +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 = { - // $ xxd -p -c32 test/programs/echo/machine-image/hash - let mut file = File::open(program_path.join("machine-image").join("hash")).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/persistent_state_access.rs b/cartesi-rollups/node/state-manager/src/persistent_state_access.rs index b11d772f..704d1a60 100644 --- a/cartesi-rollups/node/state-manager/src/persistent_state_access.rs +++ b/cartesi-rollups/node/state-manager/src/persistent_state_access.rs @@ -330,7 +330,10 @@ mod tests { let mut machine = Machine::create( &MachineConfig::new_with_ram(RAMConfig { length: 134217728, - image_filename: "../../../test/programs/linux.bin".into(), + backing_store: cartesi_machine::config::machine::BackingStoreConfig { + data_filename: "../../../test/programs/linux.bin".into(), + ..Default::default() + }, }), &RuntimeConfig::default(), ) diff --git a/cartesi-rollups/node/state-manager/src/rollups_machine.rs b/cartesi-rollups/node/state-manager/src/rollups_machine.rs index 8f013741..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, 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 = 0x7ffff000; - 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)?; + 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/cartesi-rollups/node/state-manager/src/sql/test_helper.rs b/cartesi-rollups/node/state-manager/src/sql/test_helper.rs index 73758261..d849c6b0 100644 --- a/cartesi-rollups/node/state-manager/src/sql/test_helper.rs +++ b/cartesi-rollups/node/state-manager/src/sql/test_helper.rs @@ -21,7 +21,10 @@ pub fn setup_db() -> (TempDir, Connection) { let mut machine = Machine::create( &MachineConfig::new_with_ram(RAMConfig { length: 134217728, - image_filename: "../../../test/programs/linux.bin".into(), + backing_store: cartesi_machine::config::machine::BackingStoreConfig { + data_filename: "../../../test/programs/linux.bin".into(), + ..Default::default() + }, }), &RuntimeConfig::default(), ) diff --git a/justfile b/justfile index 96d84b19..9cbb28fc 100644 --- a/justfile +++ b/justfile @@ -1,7 +1,7 @@ update-submodules: git submodule update --recursive --init -apply-generated-files-diff VERSION="v0.19.0" FILEHASH="a892e2d9f5c331f5e80bcb5db4133e7db625aa4d14ffdf9467b75c4c34d1744f": +apply-generated-files-diff VERSION="v0.20.0" FILEHASH="d9c2afcefc2759e7cd37bbedc83d54c81515f0fddb671103b489b8789aee33bb": cd machine/emulator && \ (wget -O add-generated-files.diff https://github.com/cartesi/machine-emulator/releases/download/{{VERSION}}/add-generated-files.diff && \ (echo "{{FILEHASH}} add-generated-files.diff" | sha256sum -c) && \ @@ -18,7 +18,7 @@ clean-contracts: clean-consensus-contracts clean-prt-contracts clean-bindings cl make -C machine/emulator clean depclean distclean setup: update-submodules clean-emulator clean-contracts bundle-boost apply-generated-files-diff - make -C machine/emulator # Requires docker, necessary for machine bindings + make -C machine/emulator -j8 # Requires docker, necessary for machine bindings # Run this once after cloning, if using a docker environment setup-docker: setup build-docker-image diff --git a/machine/emulator b/machine/emulator index ce402f96..8bfca691 160000 --- a/machine/emulator +++ b/machine/emulator @@ -1 +1 @@ -Subproject commit ce402f96d6757c8f4b2f08cba861cd80ab6bf834 +Subproject commit 8bfca6912f4849e03b7b55677e17e385c0b2dfbe diff --git a/machine/rust-bindings/Cargo.toml b/machine/rust-bindings/Cargo.toml index 91400111..57ea66af 100644 --- a/machine/rust-bindings/Cargo.toml +++ b/machine/rust-bindings/Cargo.toml @@ -8,7 +8,7 @@ members = [ [workspace.package] -version = "0.19.0" +version = "0.20.0" edition = "2021" license = "Apache-2.0" diff --git a/machine/rust-bindings/cartesi-machine-sys/build.rs b/machine/rust-bindings/cartesi-machine-sys/build.rs index 527c6543..bb922419 100644 --- a/machine/rust-bindings/cartesi-machine-sys/build.rs +++ b/machine/rust-bindings/cartesi-machine-sys/build.rs @@ -39,6 +39,28 @@ fn main() { } } + // OpenMP linker configuration (cross-platform) + if cfg!(target_os = "macos") { + // macOS: Try Homebrew first, then MacPorts + let homebrew_libomp = PathBuf::from("/opt/homebrew/opt/libomp"); + if homebrew_libomp.exists() { + println!("cargo:rustc-link-search={}/lib", homebrew_libomp.display()); + println!("cargo:rustc-link-lib=omp"); + } else { + let macports_libomp = PathBuf::from("/opt/local/lib/libomp"); + if macports_libomp.exists() { + println!("cargo:rustc-link-search=/opt/local/lib/libomp"); + println!("cargo:rustc-link-lib=gomp"); + } else { + // Fallback: let system linker find it + println!("cargo:rustc-link-lib=omp"); + } + } + } else { + // Linux and other Unix-like systems: libgomp comes with GCC + println!("cargo:rustc-link-lib=gomp"); + } + // // Generate bindings // @@ -197,7 +219,7 @@ mod build_cm { process::{Command, Stdio}, }; - const VERSION_STRING: &str = "v0.19.0"; + const VERSION_STRING: &str = "v0.20.0"; pub fn download(machine_dir_path: &Path) { let patch_file = machine_dir_path.join("add-generated-files.diff"); diff --git a/machine/rust-bindings/cartesi-machine/src/config/machine.rs b/machine/rust-bindings/cartesi-machine/src/config/machine.rs index e599ed6f..0905483b 100644 --- a/machine/rust-bindings/cartesi-machine/src/config/machine.rs +++ b/machine/rust-bindings/cartesi-machine/src/config/machine.rs @@ -1,98 +1,105 @@ // (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; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct MachineConfig { - pub processor: ProcessorConfig, - pub ram: RAMConfig, - pub dtb: DTBConfig, - pub flash_drive: FlashDriveConfigs, - pub tlb: TLBConfig, - pub clint: CLINTConfig, - pub plic: PLICConfig, - pub htif: HTIFConfig, - pub uarch: UarchConfig, - pub cmio: CmioConfig, - pub virtio: VirtIOConfigs, -} - -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 - } +// --------------------------------------------------------------------------- +// Backing store +// --------------------------------------------------------------------------- - pub fn plic(mut self, plic: PLICConfig) -> Self { - self.plic = plic; - self - } +/// Mirror of C++ `cartesi::backing_store_config`. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct BackingStoreConfig { + pub shared: bool, + pub create: bool, + pub truncate: bool, + pub data_filename: PathBuf, + pub dht_filename: PathBuf, + pub dpt_filename: PathBuf, +} - pub fn htif(mut self, htif: HTIFConfig) -> Self { - self.htif = htif; - self - } +/// 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 { + pub backing_store: BackingStoreConfig, +} - pub fn uarch(mut self, uarch: UarchConfig) -> Self { - self.uarch = uarch; - self - } +// --------------------------------------------------------------------------- +// 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, +} - 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 { +/// 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, pub x1: u64, pub x2: u64, @@ -187,142 +194,166 @@ pub struct ProcessorConfig { pub senvcfg: u64, pub ilrsc: u64, pub iprv: u64, - #[serde(rename = "iflags_X")] - pub iflags_x: u64, - #[serde(rename = "iflags_Y")] - pub iflags_y: u64, - #[serde(rename = "iflags_H")] - pub iflags_h: u64, + 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 image_filename: PathBuf, + 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, pub entrypoint: String, - pub image_filename: PathBuf, + pub backing_store: BackingStoreConfig, } 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")] - pub start: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub length: Option, - pub image_filename: PathBuf, - pub shared: bool, + pub start: u64, + pub length: u64, + pub read_only: bool, + pub backing_store: BackingStoreConfig, } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct CmioBufferConfig { - pub image_filename: PathBuf, - pub shared: bool, +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(), + } + } } -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct VirtIOHostfwd { - pub is_udp: bool, - pub host_ip: u64, - pub guest_ip: u64, - pub host_port: u64, - pub guest_port: u64, -} +pub type FlashDriveConfigs = Vec; -pub type VirtIOHostfwdArray = Vec; +// --------------------------------------------------------------------------- +// CMIO +// --------------------------------------------------------------------------- -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub enum VirtIODeviceType { - #[default] - Console, - P9fs, - #[serde(rename = "net-user")] - NetUser, - #[serde(rename = "net-tuntap")] - NetTuntap, -} +/// Mirror of C++ `cartesi::cmio_config`. Both buffers are +/// `backing_store_config_only`. +pub type CmioBufferConfig = BackingStoreConfigOnly; -#[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, +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct CmioConfig { + pub rx_buffer: CmioBufferConfig, + pub tx_buffer: CmioBufferConfig, } -pub type FlashDriveConfigs = Vec; +// --------------------------------------------------------------------------- +// VirtIO +// --------------------------------------------------------------------------- -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TLBConfig { - pub image_filename: PathBuf, +/// 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: u16, + pub guest_port: u16, } -impl Default for TLBConfig { - fn default() -> Self { - default_config().tlb - } -} +pub type VirtIOHostfwdArray = Vec; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct CLINTConfig { - pub mtimecmp: u64, +/// 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 { + tag: String, + host_directory: String, + }, + #[serde(rename = "net-user")] + NetUser { + hostfwd: VirtIOHostfwdArray, + }, + #[serde(rename = "net-tuntap")] + NetTuntap { + iface: String, + }, } -impl Default for CLINTConfig { +impl Default for VirtIODeviceConfig { fn default() -> Self { - default_config().clint + VirtIODeviceConfig::Console } } -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PLICConfig { - pub girqpend: u64, - pub girqsrvd: u64, -} +pub type VirtIOConfigs = Vec; -impl Default for PLICConfig { - fn default() -> Self { - default_config().plic - } -} +// --------------------------------------------------------------------------- +// PMAS +// --------------------------------------------------------------------------- -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct HTIFConfig { - pub fromhost: u64, - pub tohost: u64, - pub console_getchar: bool, - pub yield_manual: bool, - pub yield_automatic: bool, -} +/// Mirror of C++ `cartesi::pmas_config` (alias for `backing_store_config_only`). +pub type PmasConfig = BackingStoreConfigOnly; -impl Default for HTIFConfig { - fn default() -> Self { - default_config().htif - } -} +// --------------------------------------------------------------------------- +// Uarch +// --------------------------------------------------------------------------- -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct UarchProcessorConfig { +/// 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, pub x1: u64, pub x2: u64, @@ -357,29 +388,33 @@ pub struct UarchProcessorConfig { pub x31: u64, pub pc: u64, pub cycle: u64, - 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")] - pub length: Option, - pub image_filename: PathBuf, +/// 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, @@ -387,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 9e8088af..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; - pub const TREE_LOG2_WORD_SIZE: u32 = CM_TREE_LOG2_WORD_SIZE; - pub const TREE_LOG2_PAGE_SIZE: u32 = CM_TREE_LOG2_PAGE_SIZE; - pub const TREE_LOG2_ROOT_SIZE: u32 = CM_TREE_LOG2_ROOT_SIZE; + pub const HASH_SIZE: u32 = CM_HASH_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_PMA_CMIO_RX_BUFFER_START as u64; - pub const RX_LOG2_SIZE: u64 = CM_PMA_CMIO_RX_BUFFER_LOG2_SIZE as u64; - pub const TX_START: u64 = CM_PMA_CMIO_TX_BUFFER_START as u64; - pub const TX_LOG2_SIZE: u64 = CM_PMA_CMIO_TX_BUFFER_LOG2_SIZE as u64; - pub const RAM_START: u64 = CM_PMA_RAM_START as u64; + 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 371ccb07..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. @@ -92,12 +137,14 @@ impl Machine { pub fn create(config: &MachineConfig, runtime_config: &RuntimeConfig) -> Result { let config_json = serialize_to_json!(&config); let runtime_config_json = serialize_to_json!(&runtime_config); + let dir_cstr = CString::new("").unwrap(); // in-memory machine let mut machine: *mut cartesi_machine_sys::cm_machine = ptr::null_mut(); let err_code = unsafe { cartesi_machine_sys::cm_create_new( config_json.as_ptr(), runtime_config_json.as_ptr(), + dir_cstr.as_ptr(), &mut machine, ) }; @@ -108,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(); @@ -116,6 +163,7 @@ impl Machine { cartesi_machine_sys::cm_load_new( dir_cstr.as_ptr(), runtime_config_json.as_ptr(), + cartesi_machine_sys::CM_SHARING_CONFIG, &mut machine, ) }; @@ -125,9 +173,18 @@ impl Machine { } /// Stores a machine instance to a directory, serializing its entire state. + /// Uses CM_SHARING_ALL so that the current machine state is written for all + /// 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 err_code = unsafe { cartesi_machine_sys::cm_store(self.machine, dir_cstr.as_ptr()) }; + let dir_cstr = path_to_cstring(dir)?; + let err_code = unsafe { + cartesi_machine_sys::cm_store( + self.machine, + dir_cstr.as_ptr(), + cartesi_machine_sys::CM_SHARING_ALL, + ) + }; check_err!(err_code)?; Ok(()) @@ -144,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, @@ -164,25 +244,20 @@ impl Machine { shared: bool, image_path: Option<&Path>, ) -> Result<()> { - let image_cstr = match image_path { - Some(path) => path_to_cstring(path), - None => CString::new("").unwrap(), - }; - - let image_ptr = if image_path.is_some() { - image_cstr.as_ptr() - } else { - ptr::null() - }; + let range_config = serde_json::json!({ + "start": start, + "length": length, + "read_only": false, + "backing_store": { + "data_filename": image_path.map(|p| p.to_string_lossy().to_string()).unwrap_or_default(), + "shared": shared + } + }); + + let range_json = serialize_to_json!(&range_config); let err_code = unsafe { - cartesi_machine_sys::cm_replace_memory_range( - self.machine, - start, - length, - shared, - image_ptr, - ) + cartesi_machine_sys::cm_replace_memory_range(self.machine, range_json.as_ptr()) }; check_err!(err_code)?; @@ -205,7 +280,7 @@ impl Machine { pub fn memory_ranges(&mut self) -> Result { let mut ranges_ptr: *const c_char = ptr::null(); let err_code = - unsafe { cartesi_machine_sys::cm_get_memory_ranges(self.machine, &mut ranges_ptr) }; + unsafe { cartesi_machine_sys::cm_get_address_ranges(self.machine, &mut ranges_ptr) }; check_err!(err_code)?; let ranges = parse_json_from_cstring!(ranges_ptr); @@ -223,13 +298,19 @@ impl Machine { } /// Obtains the proof for a node in the machine state Merkle tree. - pub fn proof(&mut self, address: u64, log2_size: u32) -> Result { + pub fn proof( + &mut self, + address: u64, + log2_target_size: u32, + log2_root_size: u32, + ) -> Result { let mut proof_ptr: *const c_char = ptr::null(); let err_code = unsafe { cartesi_machine_sys::cm_get_proof( self.machine, address, - log2_size as i32, + log2_target_size as ::std::os::raw::c_int, + log2_root_size as ::std::os::raw::c_int, &mut proof_ptr, ) }; @@ -273,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) }; @@ -299,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, @@ -407,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, @@ -419,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( @@ -457,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( @@ -542,12 +630,11 @@ 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 { cartesi_machine_sys::cm_verify_step( - ptr::null(), root_hash_before, log_filename_c.as_ptr(), mcycle_count, @@ -570,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(), @@ -590,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(), @@ -613,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(), @@ -640,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)] @@ -650,7 +785,7 @@ mod tests { use crate::{ config::{ - machine::{DTBConfig, MachineConfig, MemoryRangeConfig, RAMConfig}, + machine::{BackingStoreConfig, MachineConfig, MemoryRangeConfig, RAMConfig}, runtime::RuntimeConfig, }, constants, @@ -659,46 +794,50 @@ mod tests { }; fn make_basic_machine_config() -> MachineConfig { - MachineConfig::new_with_ram(RAMConfig { + let mut config = Machine::default_config().expect("failed to get default config"); + config.ram = RAMConfig { length: 134217728, - image_filename: "../../../test/programs/linux.bin".into(), - }) - .dtb(DTBConfig { - entrypoint: "echo Hello from inside!".to_string(), - ..Default::default() - }) - .add_flash_drive(MemoryRangeConfig { - image_filename: "../../../test/programs/rootfs.ext2".into(), + backing_store: BackingStoreConfig { + data_filename: "../../../test/programs/linux.bin".into(), + ..Default::default() + }, + }; + config.dtb.entrypoint = "echo Hello from inside!".to_string(); + config.flash_drive = vec![MemoryRangeConfig { + backing_store: BackingStoreConfig { + data_filename: "../../../test/programs/rootfs.ext2".into(), + ..Default::default() + }, ..Default::default() - }) + }]; + config } fn make_cmio_machine_config() -> MachineConfig { - MachineConfig::new_with_ram(RAMConfig { + let mut config = Machine::default_config().expect("failed to get default config"); + config.ram = RAMConfig { length: 134217728, - image_filename: "../../../test/programs/linux.bin".into(), - }) - .dtb(DTBConfig { - entrypoint: - "echo '{\"domain\":16,\"id\":\"'$(echo -n Hello from inside! | hex --encode)'\"}' \ + backing_store: BackingStoreConfig { + data_filename: "../../../test/programs/linux.bin".into(), + ..Default::default() + }, + }; + config.dtb.entrypoint = + "echo '{\"domain\":16,\"id\":\"'$(echo -n Hello from inside! | hex --encode)'\"}' \ | rollup gio | grep -Eo '0x[0-9a-f]+' | tr -d '\\n' | hex --decode; echo" - .to_string(), + .to_string(); + config.flash_drive = vec![MemoryRangeConfig { + backing_store: BackingStoreConfig { + data_filename: "../../../test/programs/rootfs.ext2".into(), + ..Default::default() + }, ..Default::default() - }) - .add_flash_drive(MemoryRangeConfig { - image_filename: "../../../test/programs/rootfs.ext2".into(), - ..Default::default() - }) + }]; + config } 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] @@ -918,7 +1057,11 @@ mod tests { assert!(mem.iter().all(|x| *x == 1)); let log2_size = u64::BITS - range.length.leading_zeros(); - let proof: Proof = machine.proof(range.start, u64::BITS - range.length.leading_zeros())?; + let proof: Proof = machine.proof( + range.start, + u64::BITS - range.length.leading_zeros(), + 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/machine/step b/machine/step index e8050ddf..3f5d163d 160000 --- a/machine/step +++ b/machine/step @@ -1 +1 @@ -Subproject commit e8050ddfdb986d6014bebdfc1d33e16cb3b2528a +Subproject commit 3f5d163df0f7564fef3345fc919252a371e5fb9f diff --git a/prt/client-lua/computation/constants.lua b/prt/client-lua/computation/constants.lua index 1a7c9f44..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 = 0x7ffff000 +-- 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-lua/computation/machine.lua b/prt/client-lua/computation/machine.lua index e292f87e..655d883a 100644 --- a/prt/client-lua/computation/machine.lua +++ b/prt/client-lua/computation/machine.lua @@ -334,7 +334,7 @@ function Machine:prove_read_leaf(address) return data end -local keccak = require "cartesi".keccak +local keccak = cartesi.keccak256 function Machine:prove_write_leaf(address) -- always write aligned 32 bytes (one leaf) diff --git a/prt/client-lua/cryptography/hash.lua b/prt/client-lua/cryptography/hash.lua index 30638be4..e334a9a9 100644 --- a/prt/client-lua/cryptography/hash.lua +++ b/prt/client-lua/cryptography/hash.lua @@ -1,4 +1,5 @@ -local keccak = require "cartesi".keccak +local cartesi = require "cartesi" +local keccak = cartesi.keccak256 local conversion = require "utils.conversion" local interned_hashes = {} 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 74801ed6..13fe5719 100644 --- a/prt/client-rs/core/src/machine/instance.rs +++ b/prt/client-rs/core/src/machine/instance.rs @@ -1,14 +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}, + config::runtime::RuntimeConfig, + constants::machine::HASH_TREE_LOG2_ROOT_SIZE, machine::Machine, types::access_proof::AccessLog, types::{LogType, cmio::CmioResponseReason}, @@ -63,15 +64,9 @@ pub struct MachineInstance { pub snapshot_path: PathBuf, } -const CHECKPOINT_ADDRESS: u64 = 0x7ffff000; 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)?; @@ -109,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()?; @@ -256,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)?; } @@ -332,7 +317,9 @@ impl MachineInstance { // always read aligned 32 bytes (one leaf) let aligned_address = address & !0x1Fu64; let mut read = self.machine.read_memory(aligned_address, 32)?; - let proof = self.machine.proof(aligned_address, 5)?; + let proof = self + .machine + .proof(aligned_address, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let mut encoded: Vec = Vec::new(); @@ -350,7 +337,9 @@ impl MachineInstance { let aligned_address = address & !0x1Fu64; let mut read = self.machine.read_memory(aligned_address, 32)?; let read_hash = Digest::from_data(&read); - let proof = self.machine.proof(aligned_address, 5)?; + let proof = self + .machine + .proof(aligned_address, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let mut encoded: Vec = Vec::new(); @@ -370,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)?; + let proof = self.machine.proof(address, 5, HASH_TREE_LOG2_ROOT_SIZE)?; let mut encoded: Vec = Vec::new(); diff --git a/prt/contracts/src/state-transition/CartesiStateTransition.sol b/prt/contracts/src/state-transition/CartesiStateTransition.sol index eb6147bb..c3f2b713 100644 --- a/prt/contracts/src/state-transition/CartesiStateTransition.sol +++ b/prt/contracts/src/state-transition/CartesiStateTransition.sol @@ -111,7 +111,7 @@ contract CartesiStateTransition is IStateTransition { // * step // * reset // * advanceStatus - // * getCheckpointHash (only if needed) + // * getRevertRootHash (only if needed) AccessLogs.Context memory accessLogs = AccessLogs.Context(machineState, Buffer.Context(proofs, 0)); diff --git a/prt/contracts/src/state-transition/CmioStateTransition.sol b/prt/contracts/src/state-transition/CmioStateTransition.sol index a43cebc9..ff11277a 100644 --- a/prt/contracts/src/state-transition/CmioStateTransition.sol +++ b/prt/contracts/src/state-transition/CmioStateTransition.sol @@ -31,7 +31,7 @@ contract CmioStateTransition is ICmioStateTransition { pure returns (AccessLogs.Context memory) { - a.setCheckpointHash(checkpointState); + a.setRevertRootHash(checkpointState); return a; } @@ -41,7 +41,7 @@ contract CmioStateTransition is ICmioStateTransition { returns (AccessLogs.Context memory) { if (a.advanceStatus() == AdvanceStatus.Status.REJECTED) { - bytes32 checkpointState = a.getCheckpointHash(); + bytes32 checkpointState = a.getRevertRootHash(); a.currentRootHash = checkpointState; } diff --git a/prt/measure_constants/Dockerfile b/prt/measure_constants/Dockerfile index 09eace3f..6f3364b4 100644 --- a/prt/measure_constants/Dockerfile +++ b/prt/measure_constants/Dockerfile @@ -1,4 +1,4 @@ -FROM cartesi/machine-emulator:0.19.0 +FROM cartesi/machine-emulator:0.20.0 USER root RUN apt-get update && \ diff --git a/prt/tests/common/runners/helpers/patched_commitment.lua b/prt/tests/common/runners/helpers/patched_commitment.lua index 81820f40..a46bf4ae 100644 --- a/prt/tests/common/runners/helpers/patched_commitment.lua +++ b/prt/tests/common/runners/helpers/patched_commitment.lua @@ -32,7 +32,7 @@ local function filter_map_patches(patches, base_cycle, log2_stride, log2_stride_ local span = bint256.one() << (log2_stride_count + log2_stride) local mask = (bint256.one() << log2_stride) - 1 if (patch.meta_cycle & mask):iszero() and -- alignment; first bits are zero - patch.meta_cycle > base_cycle and -- meta_cycle is within lower bound + patch.meta_cycle > base_cycle and -- meta_cycle is within lower bound patch.meta_cycle <= base_cycle + span -- meta_cycle is within upper bounds then local position = ((patch.meta_cycle - base_cycle) >> log2_stride) - 1 diff --git a/prt/tests/rollups/dave/node.lua b/prt/tests/rollups/dave/node.lua index 7ff56ae7..b1d828e1 100644 --- a/prt/tests/rollups/dave/node.lua +++ b/prt/tests/rollups/dave/node.lua @@ -61,6 +61,7 @@ sqlite3 -readonly ./_state/%d/db \ 'SELECT repetitions, HEX(leaf) FROM leafs WHERE level=0 ORDER BY leaf_index ASC' 2>&1 ]] function Dave:root_commitment(epoch_index) + print(string.format("[Dave] root_commitment(epoch_index=%d) called", epoch_index)) local query = function() assert(db_exists(epoch_index), string.format("db %d doesn't exist ", epoch_index)) @@ -87,14 +88,21 @@ function Dave:root_commitment(epoch_index) builder:add(leaf, repetitions) end - return initial_state, builder:build(initial_state.root_hash) + local commitment = builder:build(initial_state.root_hash) + print(string.format("[Dave] root_commitment(epoch_index=%d) -> root=%s", epoch_index, commitment.root_hash:hex_string())) + return initial_state, commitment end local initial_state, commitment + local attempt = 0 time.sleep_until(function() + attempt = attempt + 1 self.sender:advance_blocks(1) local ok ok, initial_state, commitment = pcall(query) + if not ok and (attempt == 1 or attempt % 10 == 0) then + print(string.format("[Dave] root_commitment(epoch_index=%d) attempt %d failed: %s", epoch_index, attempt, tostring(initial_state))) + end return ok end, 5) diff --git a/prt/tests/rollups/justfile b/prt/tests/rollups/justfile index 260ed98c..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 -p -c32 ../../../test/programs/{{PROGRAM}}/machine-image/hash` \ + 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` \ diff --git a/prt/tests/rollups/test_cases/simple.lua b/prt/tests/rollups/test_cases/simple.lua index 7f57553c..3c790697 100755 --- a/prt/tests/rollups/test_cases/simple.lua +++ b/prt/tests/rollups/test_cases/simple.lua @@ -5,7 +5,7 @@ local env = require "test_env" -- Main Execution -env.spawn_blockchain {env.sample_inputs[1]} +env.spawn_blockchain { env.sample_inputs[1] } local first_epoch = assert(env.reader:read_epochs_sealed()[1]) assert(first_epoch.input_upper_bound == 0) -- there's no input for epoch 0! diff --git a/prt/tests/rollups/test_env.lua b/prt/tests/rollups/test_env.lua index b34b1dee..a6d6a2fd 100644 --- a/prt/tests/rollups/test_env.lua +++ b/prt/tests/rollups/test_env.lua @@ -56,10 +56,12 @@ function Env.spawn_blockchain(inputs) local blockchain = Blockchain:new(ANVIL_LOAD_PATH, ANVIL_DUMP_PATH) Env.blockchain = blockchain - Env.reader = Reader:new(INPUT_BOX_ADDRESS, DAVE_APP_FACTORY_ADDRESS, TEMPLATE_MACHINE_HASH, SALT, blockchain.endpoint) + Env.reader = Reader:new(INPUT_BOX_ADDRESS, DAVE_APP_FACTORY_ADDRESS, TEMPLATE_MACHINE_HASH, SALT, blockchain + .endpoint) Env.app_address = Env.reader.app_address Env.consensus_address = Env.reader.consensus_address - Env.sender = Sender:new(INPUT_BOX_ADDRESS, DAVE_APP_FACTORY_ADDRESS, Env.app_address, blockchain.pks[1], blockchain.endpoint) + Env.sender = Sender:new(INPUT_BOX_ADDRESS, DAVE_APP_FACTORY_ADDRESS, Env.app_address, blockchain.pks[1], + blockchain.endpoint) Env.sender:tx_new_dave_app(TEMPLATE_MACHINE_HASH, SALT) Env.sender:tx_add_inputs(inputs) Env.sender:advance_blocks(2) diff --git a/test/programs/justfile b/test/programs/justfile index c5acdb2d..6f30ba3a 100644 --- a/test/programs/justfile +++ b/test/programs/justfile @@ -1,7 +1,7 @@ download-deps: clean-deps wget https://github.com/cartesi/image-kernel/releases/download/v0.20.0/linux-6.5.13-ctsi-1-v0.20.0.bin \ -O ./linux.bin - wget https://github.com/cartesi/machine-emulator-tools/releases/download/v0.17.1/rootfs-tools.ext2 \ + wget https://github.com/cartesi/machine-emulator-tools/releases/download/v0.17.2/rootfs-tools.ext2 \ -O ./rootfs.ext2 clean-deps: @@ -16,16 +16,16 @@ clean-program prog: # yield build-yield: clean-yield - cartesi-machine --ram-image=./linux.bin \ - --flash-drive=label:root,filename:./rootfs.ext2 \ + cartesi-machine --ram-image=./linux.bin --final-hash \ + --flash-drive=label:root,data_filename:./rootfs.ext2 \ --no-rollback --store=./yield/machine-image \ -- "while true; do yield manual rx-accepted; yield manual rx-rejected; done" clean-yield: (clean-program "yield") # echo build-echo: clean-echo - cartesi-machine --ram-image=./linux.bin \ - --flash-drive=label:root,filename:./rootfs.ext2 \ + cartesi-machine --ram-image=./linux.bin --final-hash \ + --flash-drive=label:root,data_filename:./rootfs.ext2 \ --no-rollback --store=./echo/machine-image \ -- "ioctl-echo-loop --vouchers=1 --notices=1 --reports=1 --verbose=1 --reject=2" clean-echo: (clean-program "echo") @@ -34,6 +34,9 @@ clean-echo: (clean-program "echo") build-honeypot-snapshot: clean-honeypot-snapshot clean-honeypot-project git clone https://github.com/cartesi/honeypot.git honeypot/project git -C honeypot/project reset --hard 34d00721a527eeb7ed8cce2a13a142e3d8de9aad # v3.0.0 + sed -i 's/,filename:/,data_filename:/g' honeypot/project/Makefile + sed -i 's/--append-bootargs=ro/--append-bootargs=rw/g' honeypot/project/Makefile + sed -i 's/label:state,length:4096,user:dapp/label:state,length:4096,user:dapp,mke2fs:false,mount:false/g' honeypot/project/Makefile mkdir -p honeypot/project/config/devnet ./honeypot/generate-devnet-honeypot-config.sh > honeypot/project/config/devnet/honeypot-config.hpp make -C honeypot/project snapshot HONEYPOT_CONFIG=devnet @@ -44,8 +47,8 @@ clean-honeypot-project: # compute build-compute: clean-compute - cartesi-machine --ram-image=./linux.bin \ - --flash-drive=label:root,filename:./rootfs.ext2 \ + cartesi-machine --ram-image=./linux.bin --final-hash \ + --flash-drive=label:root,data_filename:./rootfs.ext2 \ --no-rollback --store=./compute/machine-image \ --max-mcycle=0 \ -- "while dd if=/dev/zero bs=1M count=64 2>/dev/null | md5sum >/dev/null; do :; done"