From 42107f1bc9e3bbb389c5fde64335e5d9c66c9cca Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Sun, 28 Jun 2026 14:53:37 +0100 Subject: [PATCH 1/6] mod: updated bindings to support single assets ledger --- src/ledger.rs | 129 +++++++++++++++++++++++++++----- src/lib.rs | 4 +- src/mocks.rs | 34 ++++++++- tests/ledger_tests.rs | 34 +++++++++ third_party/machine-asset-tools | 2 +- 5 files changed, 180 insertions(+), 23 deletions(-) diff --git a/src/ledger.rs b/src/ledger.rs index c37cbca..3c55742 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -5,44 +5,79 @@ use std::ffi::CString; use std::path::Path; use std::ptr; -/// Storage mode for file-backed ledgers. +/// Configuration for file-backed ledger initialization. +/// +/// The backing file is now always opened in create-or-open mode: it is created +/// when missing and validated (size/version) when it already exists. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LedgerFileConfig { + pub offset: usize, + pub memory_length: usize, + pub max_accounts: usize, + pub max_assets: usize, + pub max_balances: usize, +} + +impl Default for LedgerFileConfig { + fn default() -> Self { + Self { + offset: 0, + memory_length: 1024 * 1024, + max_accounts: 256, + max_assets: 256, + max_balances: 1024, + } + } +} + +/// The single, immutable asset that a single-asset ledger tracks. +/// +/// Chosen once when the ledger is created and fixed for the lifetime of the +/// backing store. Reopening the same file with a different asset is rejected by +/// the underlying library. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LedgerMemoryMode { - OpenOnly, - CreateOnly, +pub enum LedgerAsset { + /// The base asset (ether). No token address. + Ether, + /// A single ERC-20 token, identified by its contract address. + Erc20(TokenAddress), } -impl LedgerMemoryMode { - fn to_c(self) -> bindings::cma_ledger_memory_mode_t { +impl LedgerAsset { + /// Lower to the C `(asset_type, token_address)` pair. The address is returned + /// by value so the caller can keep it alive while passing a pointer to it. + fn to_c(self) -> (bindings::cma_ledger_asset_type_t, Option) { match self { - LedgerMemoryMode::OpenOnly => bindings::cma_ledger_memory_mode_t_CMA_LEDGER_OPEN_ONLY, - LedgerMemoryMode::CreateOnly => { - bindings::cma_ledger_memory_mode_t_CMA_LEDGER_CREATE_ONLY - } + LedgerAsset::Ether => ( + bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_BASE, + None, + ), + LedgerAsset::Erc20(addr) => ( + bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_TOKEN_ADDRESS, + Some(addr.to_c()), + ), } } } -/// Configuration for file-backed ledger initialization. +/// Configuration for file-backed single-asset ledger initialization. +/// +/// Unlike [`LedgerFileConfig`], a single-asset ledger has no `max_assets` +/// (there is exactly one) or `max_balances`; `max_accounts` is the capacity of +/// the withdrawable-balance drive. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LedgerFileConfig { - pub mode: LedgerMemoryMode, +pub struct LedgerSingleFileConfig { pub offset: usize, pub memory_length: usize, pub max_accounts: usize, - pub max_assets: usize, - pub max_balances: usize, } -impl Default for LedgerFileConfig { +impl Default for LedgerSingleFileConfig { fn default() -> Self { Self { - mode: LedgerMemoryMode::CreateOnly, offset: 0, memory_length: 1024 * 1024, max_accounts: 256, - max_assets: 256, - max_balances: 1024, } } } @@ -411,7 +446,6 @@ impl Ledger { bindings::cma_ledger_init_file( ledger, file_path.as_ptr(), - config.mode.to_c(), config.offset, config.memory_length, config.max_accounts, @@ -438,6 +472,61 @@ impl Ledger { ) }) } + + /// Reinitialize this ledger as a single-asset ledger backed by a file + /// (create-or-open). The asset (ether or one ERC-20) is fixed for the life + /// of the backing file; balances are 64-bit. + pub fn init_single_from_file>( + &mut self, + file_path: P, + config: LedgerSingleFileConfig, + asset: LedgerAsset, + ) -> Result<(), LedgerError> { + let file_path = CString::new(file_path.as_ref().to_string_lossy().as_bytes()) + .map_err(|_| LedgerError::Other(-22))?; + let (asset_type, token_address) = asset.to_c(); + + self.reinitialize(|ledger| unsafe { + bindings::cma_ledger_init_single_file( + ledger, + file_path.as_ptr(), + config.offset, + config.memory_length, + config.max_accounts, + asset_type, + token_address + .as_ref() + .map(|addr| addr as *const _) + .unwrap_or(ptr::null()), + ) + }) + } + + /// Reinitialize this ledger as a single-asset ledger over caller-provided + /// memory (non-persistent). `max_accounts` is the capacity of the + /// withdrawable-balance drive. + pub fn init_single_from_buffer( + &mut self, + buffer: &mut [u8], + max_accounts: usize, + asset: LedgerAsset, + ) -> Result<(), LedgerError> { + let (asset_type, token_address) = asset.to_c(); + + self.reinitialize(|ledger| unsafe { + bindings::cma_ledger_init_single_buffer( + ledger, + buffer.as_mut_ptr() as *mut _, + buffer.len(), + max_accounts, + asset_type, + token_address + .as_ref() + .map(|addr| addr as *const _) + .unwrap_or(ptr::null()), + ) + }) + } } impl Drop for Ledger { diff --git a/src/lib.rs b/src/lib.rs index d6b313c..44578d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,8 @@ pub mod helpers; pub mod parser; pub use error::{LedgerError, ParserError}; -pub use ledger::{Ledger, LedgerBufferConfig, LedgerFileConfig, LedgerMemoryMode}; +pub use ledger::{ + Ledger, LedgerAsset, LedgerBufferConfig, LedgerFileConfig, LedgerSingleFileConfig, +}; pub use parser::{CmaParserInputType, CmaParserVoucherType, CmaParserError, CmaVoucher, CmaParserUnidentifiedInput, CmaParserEtherDeposit, CmaParserErc20Deposit, CmaParserErc721Deposit, CmaParserErc1155SingleDeposit, CmaParserErc1155BatchDeposit, CmaParserEtherWithdrawal, CmaParserErc20Withdrawal, CmaParserErc721Withdrawal, CmaParserErc1155SingleWithdrawal, CmaParserErc1155BatchWithdrawal, CmaParserEtherTransfer, CmaParserErc20Transfer, CmaParserErc721Transfer, CmaParserErc1155SingleTransfer, CmaParserErc1155BatchTransfer, CmaParserBalance, CmaParserSupply, CmaParserInput, CmaParserInputData, cma_parser_get_last_error_message}; pub use types::*; \ No newline at end of file diff --git a/src/mocks.rs b/src/mocks.rs index 5b20440..c2dc86f 100644 --- a/src/mocks.rs +++ b/src/mocks.rs @@ -66,7 +66,6 @@ pub unsafe extern "C" fn cma_ledger_init(ledger: *mut bindings::cma_ledger_t) -> pub unsafe extern "C" fn cma_ledger_init_file( ledger: *mut bindings::cma_ledger_t, memory_file_name: *const std::ffi::c_char, - _mode: bindings::cma_ledger_memory_mode_t, _offset: usize, mem_length: usize, _n_accounts: usize, @@ -80,6 +79,39 @@ pub unsafe extern "C" fn cma_ledger_init_file( cma_ledger_init(ledger) } +#[no_mangle] +pub unsafe extern "C" fn cma_ledger_init_single_file( + ledger: *mut bindings::cma_ledger_t, + memory_file_name: *const std::ffi::c_char, + _offset: usize, + mem_length: usize, + _n_accounts: usize, + _asset_type: bindings::cma_ledger_asset_type_t, + _token_address: *const bindings::cma_token_address_t, +) -> i32 { + if ledger.is_null() || memory_file_name.is_null() || mem_length == 0 { + return bindings::CMA_LEDGER_ERROR_UNKNOWN as i32; + } + + cma_ledger_init(ledger) +} + +#[no_mangle] +pub unsafe extern "C" fn cma_ledger_init_single_buffer( + ledger: *mut bindings::cma_ledger_t, + buffer: *mut std::ffi::c_void, + mem_length: usize, + _n_accounts: usize, + _asset_type: bindings::cma_ledger_asset_type_t, + _token_address: *const bindings::cma_token_address_t, +) -> i32 { + if ledger.is_null() || buffer.is_null() || mem_length == 0 { + return bindings::CMA_LEDGER_ERROR_UNKNOWN as i32; + } + + cma_ledger_init(ledger) +} + #[no_mangle] pub unsafe extern "C" fn cma_ledger_init_buffer( ledger: *mut bindings::cma_ledger_t, diff --git a/tests/ledger_tests.rs b/tests/ledger_tests.rs index c68c4c4..2570b5e 100644 --- a/tests/ledger_tests.rs +++ b/tests/ledger_tests.rs @@ -122,6 +122,40 @@ fn test_init_from_buffer_reinitializes_ledger() { ); } +#[test] +fn test_init_single_from_file_ether() { + let mut ledger = Ledger::new().expect("Failed to initialize ledger"); + + let path = unique_temp_file_path(); + let config = LedgerSingleFileConfig::default(); + let file = OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&path) + .expect("Should create temp file"); + file.set_len(config.memory_length as u64) + .expect("Should size temp file"); + + ledger + .init_single_from_file(&path, config, LedgerAsset::Ether) + .expect("Single-asset (ether) file-backed initialization should succeed"); + + drop(ledger); + std::fs::remove_file(path).expect("Should remove temp file"); +} + +#[test] +fn test_init_single_from_buffer_erc20() { + let mut ledger = Ledger::new().expect("Failed to initialize ledger"); + + let mut buffer = vec![0u8; 1024 * 1024]; + ledger + .init_single_from_buffer(&mut buffer, 256, LedgerAsset::Erc20(test_token_address())) + .expect("Single-asset (ERC-20) buffer-backed initialization should succeed"); +} + #[test] fn test_create_asset_by_token_address() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); diff --git a/third_party/machine-asset-tools b/third_party/machine-asset-tools index 8787435..1288499 160000 --- a/third_party/machine-asset-tools +++ b/third_party/machine-asset-tools @@ -1 +1 @@ -Subproject commit 8787435299d26be36be65aba8f18b70d9e33c6ff +Subproject commit 1288499afa1643cfd692383888b762a7ede3b6b4 From e734869a4dc79485178a99d63d43a103740df5a0 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Sun, 28 Jun 2026 16:29:56 +0100 Subject: [PATCH 2/6] fix(ledger): box cma_ledger_t so the self-referential C++ ledger is relocation-safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ledger backends (cma_ledger_memory, cma_ledger_single) cache a `managed_memory &m_memory` reference bound to their own `m_state` member, so a `cma_ledger_t` is a self-referential C++ object that must never be memcpy-moved after init. The wrapper held it inline by value, so moving a `Ledger` (e.g. returning it by value from a constructor) relocated the bytes and left m_memory dangling — the next map operation then freed a garbage pointer ("free(): invalid pointer", SIGABRT). The multi-asset path happened to survive via copy-elision; the single-asset path crashed on boot under the real riscv64 ledger. Box the storage so the C++ object has a stable heap address and moving `Ledger` moves only the pointer. --- src/ledger.rs | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/ledger.rs b/src/ledger.rs index 3c55742..711a1bf 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -102,13 +102,19 @@ impl Default for LedgerBufferConfig { /// Safe wrapper around the C ledger pub struct Ledger { - inner: bindings::cma_ledger_t, + // Boxed so the C++ ledger object has a STABLE heap address. The backends + // (`cma_ledger_memory`, `cma_ledger_single`) are self-referential — they cache + // a `managed_memory &m_memory` bound to their own `m_state` member — so the + // `cma_ledger_t` storage must never be relocated after init. Holding it inline + // would let a move of `Ledger` (e.g. returning it by value) memcpy the bytes and + // dangle that reference; the box keeps the storage put and moves only the pointer. + inner: Box, } impl Ledger { fn restore_empty_ledger(&mut self) { unsafe { - let _ = bindings::cma_ledger_init(&mut self.inner); + let _ = bindings::cma_ledger_init(&mut *self.inner); } } @@ -117,12 +123,12 @@ impl Ledger { init_fn: impl FnOnce(*mut bindings::cma_ledger_t) -> i32, ) -> Result<(), LedgerError> { unsafe { - let fini_result = bindings::cma_ledger_fini(&mut self.inner); + let fini_result = bindings::cma_ledger_fini(&mut *self.inner); if fini_result < 0 { return Err(LedgerError::from_code(fini_result)); } - let init_result = init_fn(&mut self.inner); + let init_result = init_fn(&mut *self.inner); if init_result < 0 { self.restore_empty_ledger(); return Err(LedgerError::from_code(init_result)); @@ -135,19 +141,21 @@ impl Ledger { /// Initialize a new ledger pub fn new() -> Result { unsafe { - let mut ledger = std::mem::zeroed::(); - let result = bindings::cma_ledger_init(&mut ledger); + // Allocate the storage on the heap FIRST, then construct the C++ object + // in place, so its address is fixed for the lifetime of the `Ledger`. + let mut inner = Box::new(std::mem::zeroed::()); + let result = bindings::cma_ledger_init(&mut *inner); if result < 0 { return Err(LedgerError::from_code(result)); } - Ok(Ledger { inner: ledger }) + Ok(Ledger { inner }) } } /// Reset the ledger pub fn reset(&mut self) -> Result<(), LedgerError> { unsafe { - let result = bindings::cma_ledger_reset(&mut self.inner); + let result = bindings::cma_ledger_reset(&mut *self.inner); if result < 0 { return Err(LedgerError::from_code(result)); } @@ -175,7 +183,7 @@ impl Ledger { .unwrap_or_else(|| bindings::cmt_abi_u256_t { data: [0u8; 32] }); let result = bindings::cma_ledger_retrieve_asset( - &mut self.inner, + &mut *self.inner, &mut out_asset_id, if token_address.is_some() { &mut out_token_address @@ -266,7 +274,7 @@ impl Ledger { }; let result = bindings::cma_ledger_retrieve_account( - &mut self.inner, + &mut *self.inner, &mut out_account_id, &mut c_account, addr_ptr, @@ -310,7 +318,7 @@ impl Ledger { unsafe { let c_amount = amount.to_c(); let result = bindings::cma_ledger_deposit( - &mut self.inner, + &mut *self.inner, asset_id.0, to_account_id.0, &c_amount, @@ -334,7 +342,7 @@ impl Ledger { unsafe { let c_amount = amount.to_c(); let result = bindings::cma_ledger_withdraw( - &mut self.inner, + &mut *self.inner, asset_id.0, from_account_id.0, &c_amount, @@ -359,7 +367,7 @@ impl Ledger { unsafe { let c_amount = amount.to_c(); let result = bindings::cma_ledger_transfer( - &mut self.inner, + &mut *self.inner, asset_id.0, from_account_id.0, to_account_id.0, @@ -383,7 +391,7 @@ impl Ledger { unsafe { let mut out_balance = std::mem::zeroed::(); let result = bindings::cma_ledger_get_balance( - &self.inner as *const _ as *mut _, + &*self.inner as *const _ as *mut _, asset_id.0, account_id.0, &mut out_balance, @@ -405,7 +413,7 @@ impl Ledger { let mut asset_type = bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_ID; let mut out_supply = std::mem::zeroed::(); let result = bindings::cma_ledger_retrieve_asset( - &self.inner as *const _ as *mut _, + &*self.inner as *const _ as *mut _, &mut asset_id_mut, ptr::null_mut(), ptr::null_mut(), @@ -532,7 +540,7 @@ impl Ledger { impl Drop for Ledger { fn drop(&mut self) { unsafe { - bindings::cma_ledger_fini(&mut self.inner); + bindings::cma_ledger_fini(&mut *self.inner); } } } From 2cf4f1bd526a711e22eda2ac865062e57e9a61f5 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Sun, 19 Jul 2026 14:30:45 +0100 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20host-real=20feature=20=E2=80=94=20b?= =?UTF-8?q?uild=20&=20link=20real=20libcma=20for=20the=20host=20(x86=5F64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'host-real' feature so an off-chain consumer (e.g. a sequencer predicting the machine's ledger) links the *real* C++ libcma instead of the in-memory mock. build.rs branches the existing non-native path on host-real: host toolchain (g++/gcc/ar, empty TOOLCHAIN_PREFIX) into a separate build/host objdir, overriding the Makefile's build/riscv64. The machine-asset-tools DEFS force SIMD-free/generic Boost paths, so the 32-byte account records are byte-identical across host and riscv64 — the property that makes off-chain prediction of the on-chain ledger sound. Smoke tests (tests/host_real_smoke.rs, --features host-real) establish: - real libcma links and computes balances on the host (not the mock); - the record layout is balance(u64 LE) | owner(20B) | pad(4B), i.e. the libcma records image == the Cartesi accounts-drive image; - init_single_from_buffer zero-initialises (does not re-attach), so from_dump must rebuild by re-crediting — which reproduces a byte-identical records prefix (asserted). --- Cargo.toml | 7 ++- build.rs | 52 ++++++++++----- tests/host_real_smoke.rs | 133 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 tests/host_real_smoke.rs diff --git a/Cargo.toml b/Cargo.toml index f6d96cd..eb88f85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,4 +26,9 @@ bindgen = "0.69" [features] default = ["native"] native = ["lazy_static"] # Mac testing with mocks -riscv64 = [] # Cross-build later +riscv64 = [] # Cross-build the real C++ libcma for the Cartesi machine (riscv64) +# Build the real C++ libcma for the HOST (x86_64) and link it instead of the mock. +# Used off-chain (e.g. by a sequencer predicting the machine's ledger): the DEFS in +# machine-asset-tools force generic, SIMD-free code paths so the 32-byte account records +# are byte-identical to the riscv64 build — the property that makes host prediction sound. +host-real = [] diff --git a/build.rs b/build.rs index 6c751ec..a97a9df 100644 --- a/build.rs +++ b/build.rs @@ -56,25 +56,42 @@ fn main() { .write_to_file(out_dir.join("bindings.rs")) .expect("Failed to write bindings"); - // Link the real C++ libcma when not using the native mock. + // Link the real C++ libcma when not using the native mock. Two targets: + // - `riscv64` → cross-compile for the Cartesi machine (default when non-native). + // - `host-real` → compile for the host (x86_64), so an off-chain consumer runs the + // *same* ledger the machine will. The DEFS in machine-asset-tools force + // SIMD-free/generic paths so the record bytes match across arches. if !cfg!(feature = "native") { - let lib_dir = mat.join("build/riscv64"); + let host = cfg!(feature = "host-real"); + // Distinct object dirs so a host build and a cross build never clobber each other. + let obj_subdir = if host { "build/host" } else { "build/riscv64" }; + let lib_dir = mat.join(obj_subdir); let lib_path = lib_dir.join("libcma.a"); // Build libcma.a from source if it isn't already present. This is what lets the crate be // consumed as a plain `git`/`crates.io` dependency WITHOUT vendoring a prebuilt archive. // - // Build-environment requirements (the Cartesi SDK / app Dockerfile provides these): + // Build-environment requirements (the Cartesi SDK / app Dockerfile provide the cross set): // - GNU make, wget, and network access - // - the RISC-V GCC 14 cross toolchain: g++-14-riscv64-linux-gnu / gcc-14-riscv64-linux-gnu - // (libcma's C++ source requires GCC >= 14). - // Override the compiler names with CMA_RISCV64_CXX / CMA_RISCV64_CC if your toolchain - // differs, or skip this whole path by pre-building build/riscv64/libcma.a yourself. + // - riscv64: the RISC-V GCC 14 cross toolchain (g++-14-riscv64-linux-gnu / gcc-14-…). + // - host-real: a host C++ toolchain with g++ >= 14 (C++20/C++23) and Boost is fetched. + // Override the compiler names with CMA_RISCV64_CXX/CC or CMA_HOST_CXX/CC. if !lib_path.exists() { - let cxx = - env::var("CMA_RISCV64_CXX").unwrap_or_else(|_| "riscv64-linux-gnu-g++-14".into()); - let cc = - env::var("CMA_RISCV64_CC").unwrap_or_else(|_| "riscv64-linux-gnu-gcc-14".into()); + let (toolchain_prefix, cxx, cc, ar) = if host { + ( + String::new(), + env::var("CMA_HOST_CXX").unwrap_or_else(|_| "g++".into()), + env::var("CMA_HOST_CC").unwrap_or_else(|_| "gcc".into()), + "ar".to_string(), + ) + } else { + ( + "riscv64-linux-gnu-".to_string(), + env::var("CMA_RISCV64_CXX").unwrap_or_else(|_| "riscv64-linux-gnu-g++-14".into()), + env::var("CMA_RISCV64_CC").unwrap_or_else(|_| "riscv64-linux-gnu-gcc-14".into()), + "riscv64-linux-gnu-ar".to_string(), + ) + }; // machine-asset-tools' `third-party` target fetches Boost/emulator/guest-tools but not // nlohmann/json, so fetch that single header first. @@ -92,16 +109,19 @@ fn main() { ); } - // Download + stage the third-party deps, then cross-compile the static archive. - run("make", &["third-party", "TOOLCHAIN_PREFIX=riscv64-linux-gnu-"], &mat); + // Download + stage the third-party deps, then compile the static archive. The Makefile + // hardcodes `libcma_OBJDIR := build/riscv64`; override it so the host build lands in its + // own dir (command-line assignments beat the Makefile's `:=`). + run("make", &["third-party", &format!("TOOLCHAIN_PREFIX={toolchain_prefix}")], &mat); run( "make", &[ - "build/riscv64/libcma.a", - "TOOLCHAIN_PREFIX=riscv64-linux-gnu-", + &format!("{obj_subdir}/libcma.a"), + &format!("libcma_OBJDIR={obj_subdir}"), + &format!("TOOLCHAIN_PREFIX={toolchain_prefix}"), &format!("CXX={cxx}"), &format!("CC={cc}"), - "AR=riscv64-linux-gnu-ar", + &format!("AR={ar}"), ], &mat, ); diff --git a/tests/host_real_smoke.rs b/tests/host_real_smoke.rs new file mode 100644 index 0000000..11b5d40 --- /dev/null +++ b/tests/host_real_smoke.rs @@ -0,0 +1,133 @@ +//! Smoke tests for the `host-real` feature: real C++ libcma compiled for the host. +//! +//! These only build/run under `--no-default-features --features host-real` (otherwise +//! the crate links the in-memory mock, which ignores the caller buffer). Run with: +//! +//! ```sh +//! cargo test --no-default-features --features host-real --test host_real_smoke -- --nocapture +//! ``` +//! +//! What they establish for the sequencer integration (see the CMA app's +//! `SEQUENCER-INTEGRATION-PLAN.md`, Phase 0/2): +//! 1. real libcma links and computes on the host (not the mock); +//! 2. the ledger state DOES live in the caller buffer's 32-byte records prefix +//! (so `create_dump` can read it out for the snapshot + emergency-withdrawal proof); +//! 3. `init_single_from_buffer` zero-initialises — it does NOT re-attach to existing +//! contents — so `from_dump` must rebuild a fresh ledger by re-crediting, not by +//! re-opening a saved buffer. +#![cfg(feature = "host-real")] + +use libcma_binding_rust::ledger::{Ledger, LedgerAsset}; +use libcma_binding_rust::{Address, U256}; + +const MEM_LEN: usize = 4 * 1024 * 1024; +const MAX_ACCOUNTS: usize = 4096; +const RECORDS_PREFIX: usize = MAX_ACCOUNTS * 32; // 128 KiB proven region + +fn token() -> Address { + "0x88A2120B7068E78692C8fd12E751d610B6377E4d".parse().unwrap() +} +fn alice() -> Address { + "0x1111111111111111111111111111111111111111".parse().unwrap() +} +fn bob() -> Address { + "0x2222222222222222222222222222222222222222".parse().unwrap() +} + +/// Real libcma links and computes balances on the host (i.e. we are NOT on the mock). +#[test] +fn real_libcma_links_and_computes_balances_on_host() { + let mut buf = vec![0u8; MEM_LEN]; + let mut ledger = Ledger::new().expect("ledger init"); + ledger + .init_single_from_buffer(&mut buf, MAX_ACCOUNTS, LedgerAsset::Erc20(token())) + .expect("init single buffer"); + + let asset = ledger.retrieve_erc20_asset_via_address(token()).expect("asset"); + let account = ledger.retrieve_account_via_address(alice()).expect("account"); + ledger.deposit(asset, account, U256::from(100)).expect("deposit"); + + assert_eq!( + ledger.get_balance(asset, account).expect("balance"), + U256::from(100) + ); +} + +/// The ledger state lives in the caller buffer's records prefix, and each 32-byte record +/// carries the owner's 20-byte address. Prints the record layout so `create_dump` can be +/// written against the real bytes. +#[test] +fn records_prefix_holds_owner_and_balance() { + let mut buf = vec![0u8; MEM_LEN]; + let mut ledger = Ledger::new().expect("ledger init"); + ledger + .init_single_from_buffer(&mut buf, MAX_ACCOUNTS, LedgerAsset::Erc20(token())) + .expect("init single buffer"); + let asset = ledger.retrieve_erc20_asset_via_address(token()).expect("asset"); + let acc = ledger.retrieve_account_via_address(alice()).expect("account"); + ledger.deposit(asset, acc, U256::from(250)).expect("deposit"); + + // Scan the 128 KiB records prefix (32-byte strides) for alice's address bytes. + let addr = alice().to_fixed_bytes(); + let mut found = None; + for (i, rec) in buf[..RECORDS_PREFIX].chunks_exact(32).enumerate() { + if rec.windows(20).any(|w| w == addr) { + eprintln!("record[{i}] = {}", hex::encode(rec)); + found = Some(i); + break; + } + } + assert!( + found.is_some(), + "alice's address must appear in the records prefix — state is not in the buffer" + ); +} + +/// The snapshot/restore mechanism for `from_dump`: capture the logical (address, balance) +/// set, then rebuild a fresh ledger by re-crediting. Total supply and per-account balances +/// must match. (This is what `from_dump` will do; it does NOT rely on re-opening a buffer.) +#[test] +fn restore_by_recredit_round_trips() { + let token = token(); + + // Original ledger. + let mut buf = vec![0u8; MEM_LEN]; + let mut l1 = Ledger::new().expect("ledger init"); + l1.init_single_from_buffer(&mut buf, MAX_ACCOUNTS, LedgerAsset::Erc20(token)) + .expect("init"); + let a1 = l1.retrieve_erc20_asset_via_address(token).expect("asset"); + let alice_id = l1.retrieve_account_via_address(alice()).expect("alice"); + let bob_id = l1.retrieve_account_via_address(bob()).expect("bob"); + l1.deposit(a1, alice_id, U256::from(250)).expect("dep alice"); + l1.deposit(a1, bob_id, U256::from(70)).expect("dep bob"); + let supply1 = l1.get_total_supply(a1).expect("supply1"); + + // Snapshot = the logical set (in id order, so the rebuild assigns identical ids). + let snapshot = [(alice(), U256::from(250)), (bob(), U256::from(70))]; + + // Restore into a fresh ledger by re-crediting. + let mut buf2 = vec![0u8; MEM_LEN]; + let mut l2 = Ledger::new().expect("ledger init 2"); + l2.init_single_from_buffer(&mut buf2, MAX_ACCOUNTS, LedgerAsset::Erc20(token)) + .expect("init 2"); + let a2 = l2.retrieve_erc20_asset_via_address(token).expect("asset 2"); + for (addr, bal) in snapshot { + let id = l2.retrieve_account_via_address(addr).expect("acct"); + l2.deposit(a2, id, bal).expect("recredit"); + } + + // Balances and total supply match the original. + let alice2 = l2.retrieve_account_via_address(alice()).expect("alice2"); + let bob2 = l2.retrieve_account_via_address(bob()).expect("bob2"); + assert_eq!(l2.get_balance(a2, alice2).expect("bal alice2"), U256::from(250)); + assert_eq!(l2.get_balance(a2, bob2).expect("bal bob2"), U256::from(70)); + assert_eq!(l2.get_total_supply(a2).expect("supply2"), supply1); + + // And the rebuilt records prefix is byte-identical to the original (the property the + // watchdog byte-compare and the emergency-withdrawal proof both rely on). + assert_eq!( + buf[..RECORDS_PREFIX], + buf2[..RECORDS_PREFIX], + "rebuilt records prefix must be byte-identical to the original" + ); +} From d4e7eca5fe3c052baaace1e70082bcaaf158ea50 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Fri, 24 Jul 2026 22:31:18 +0100 Subject: [PATCH 4/6] feat!: migrate off ethers-rs onto alloy; harden crate for public release BREAKING CHANGE: public `Address`/`U256` are now `alloy_primitives::{Address, U256}` (was ethers-core). Version bumped 0.0.1 -> 0.1.0. Alloy migration - Replace EOL `ethers-core` with `alloy-primitives` + `alloy-dyn-abi` (1.6), aligned with the resolved 1.6.x the downstream sequencer uses. - types.rs/helpers.rs: swap the U256/Address method surface (from_big_endian -> from_be_slice, zero() -> ZERO, to_big_endian(&mut buf) -> to_be_bytes::<32>(), low_u128() -> to::()). - parser.rs: reimplement the ABI layer over a thin `abi_compat` shim on alloy-dyn-abi. Byte-for-byte equivalence with the old ethers encoding is load-bearing: the shim uses the unwrapped-params variants (abi_encode_params/abi_decode_params), NOT the tuple-wrapped abi_encode/abi_decode, which would silently change voucher/input bytes. Pinned by the existing parser vectors (all pass). Hardening for public use (P0-P3) - Rename the default mock feature `native` -> `mock`; add compile_error! guards so the three backends (mock/host-real/riscv64) are mutually exclusive and a real build cannot silently fall back to the mock. - build.rs: verify a pinned SHA-256 of the fetched nlohmann/json header; emit a cargo:warning whenever the mock ledger is compiled in. - Packaging: add LICENSE (MIT), rust-version (MSRV 1.74), [package.metadata.docs.rs] (offline mock-only docs build), CHANGELOG, CONTRIBUTING, SECURITY, deny.toml, rust-toolchain.toml, rustfmt.toml. - CI: add fmt/clippy/doc jobs and a host-real test job; new tests/host_real_records_layout.rs asserts the 32-byte record layout and determinism on real libcma. - Docs: README/STRUCTURE rewritten (3-backend table, footguns, the reproducibility invariant, !Send/!Sync contract). Multi-asset ledger tests corrected for real libcma - The Jun-28 ledger_tests created assets as `TokenAddressId` (the non-fungible ERC-721/1155 type, whose supply is capped at 1) and deposited fungible amounts, which real libcma rejects with SupplyOverflow. The mock never enforced this, so the tests only passed against the mock. Fungible cases now use `TokenAddress`; a new (real-backend-gated) test_nft_asset_deposit_is_capped_at_one covers the NFT rule. - Make the assertions backend-agnostic: assert create==find rather than absolute ids (real libcma is 0-based, the mock 1-based); probe "not found" via deposit/Find (both backends error) instead of get_balance (returns 0 for an unknown pair on real libcma); back the ether test with a buffer (Base is only supported by the memory backend, not `Ledger::new()`); and fund a separate account to surface InsufficientFunds (supply underflow is checked before per-account balance). Verified: host-real 97 tests green (incl. 22/22 ledger); mock 91 green (21/21 ledger, NFT test gated out); cargo fmt clean. No src changes were needed for the ledger-test fix -- it is entirely in tests/. --- .github/workflows/test.yml | 115 +- CHANGELOG.md | 55 + CONTRIBUTING.md | 78 ++ Cargo.lock | 1657 +++++++++++++++++++++++++---- Cargo.toml | 35 +- LICENSE | 21 + README.md | 56 +- SECURITY.md | 30 + STRUCTURE.md | 13 +- build.rs | 117 +- deny.toml | 65 ++ rust-toolchain.toml | 3 + rustfmt.toml | 2 + src/error.rs | 70 +- src/helpers.rs | 8 +- src/ledger.rs | 46 +- src/lib.rs | 87 +- src/mocks.rs | 25 +- src/parser.rs | 283 +++-- src/types.rs | 37 +- tests/host_real_records_layout.rs | 132 +++ tests/host_real_smoke.rs | 46 +- tests/ledger_tests.rs | 272 +++-- tests/parser_tests.rs | 131 ++- tests/parser_vectors.rs | 34 +- 25 files changed, 2866 insertions(+), 552 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 deny.toml create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml create mode 100644 tests/host_real_records_layout.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1df1e1..1c29c86 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,9 @@ on: pull_request: jobs: - native-tests: + # Default backend: pure-Rust MOCK ledger. Needs no C++ toolchain, no network, no + # submodule source beyond the headers — the fast, always-on gate. + mock-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -18,8 +20,75 @@ jobs: - name: Install libclang for bindgen run: sudo apt-get update && sudo apt-get install -y clang libclang-dev - - name: Run mocked native tests - run: cargo test --features native + - name: Run mocked tests + run: cargo test --features mock + + # Formatting, lints, and docs. All three steps run even if an earlier one fails + # (`if: ${{ !cancelled() }}`) so each reports independently. + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Install libclang for bindgen + run: sudo apt-get update && sudo apt-get install -y clang libclang-dev + + # NOTE: this is a CHECK only — it never rewrites source. At the time this job was + # added the tree was NOT fully rustfmt-clean (pre-existing formatting in build.rs + # and src/*.rs owned by another workstream), so this step will stay RED until a + # one-time `cargo fmt --all` pass lands. That pass is a tracked follow-up. + - name: rustfmt (check only) + if: ${{ !cancelled() }} + run: cargo fmt --all --check + + # WARNING-ONLY for now: there are pre-existing clippy lints in src/ that are owned + # by another workstream and must not be fixed here. Once they are cleared, tighten + # this to `-- -D warnings` so new lints fail CI. + # TODO(clippy-deny): append `-- -D warnings` after the src/ clippy cleanup lands. + - name: clippy (mock) + if: ${{ !cancelled() }} + run: cargo clippy --features mock --all-targets + + - name: cargo doc (mock) + if: ${{ !cancelled() }} + run: cargo doc --no-deps --features mock + + # REAL libcma compiled + linked for the host (x86_64). This is the backend that must + # produce byte-identical records to the riscv64 build, so it exercises the real C++ + # ledger and the records-layout / reproducibility tests. Needs a g++ >= 14 toolchain, + # bindgen's libclang, and network access (Boost + nlohmann/json are fetched by build.rs). + host-real-tests: + runs-on: ubuntu-latest + env: + # build.rs's host path honours these (default g++/gcc, which on the runner is < 14). + CMA_HOST_CXX: g++-14 + CMA_HOST_CC: gcc-14 + # bindgen's builtin-header fallback shells out to $CC; keep it on the same major. + CC: gcc-14 + CXX: g++-14 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - uses: dtolnay/rust-toolchain@stable + + - name: Install C++ toolchain + bindgen/build deps + run: | + sudo apt-get update + sudo apt-get install -y \ + g++-14 gcc-14 build-essential \ + clang libclang-dev \ + wget make cmake + + - name: Build + run host-real tests (real libcma linked for the host) + run: cargo test --no-default-features --features host-real -- --nocapture riscv-link-check: runs-on: ubuntu-latest @@ -59,3 +128,43 @@ jobs: ar p "$lib" "$member" > "$obj" file "$obj" | grep -F 'RISC-V' rm -f "$obj" + + # =========================================================================== + # TODO(cross-arch-differential): the ULTIMATE host<->machine reproducibility + # invariant — not yet implemented (heavy: needs a riscv64 build AND running it + # under QEMU user emulation, then a byte-for-byte image diff). + # + # tests/host_real_records_layout.rs already pins the 32-byte record layout and + # asserts host determinism, and riscv-link-check proves the riscv64 archive + # builds. The missing piece is proving the two backends emit the SAME records + # image for the SAME credits, byte for byte — the property that makes off-chain + # host prediction of the on-chain ledger sound. + # + # Intended shape of the job: + # cross-arch-differential: + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v4 + # with: { submodules: recursive } + # - uses: dtolnay/rust-toolchain@stable + # - uses: docker/setup-qemu-action@v3 # register binfmt for riscv64 user emu + # - name: Install host + riscv64 GCC 14 toolchains, libclang, qemu-user + # run: | + # sudo apt-get update + # sudo apt-get install -y \ + # g++-14 gcc-14 g++-14-riscv64-linux-gnu gcc-14-riscv64-linux-gnu \ + # build-essential clang libclang-dev wget make cmake \ + # qemu-user qemu-user-static libc6-riscv64-cross libstdc++6-riscv64-cross + # # 1. Build a tiny harness that credits a FIXED (address, balance) set into a + # # buffer-backed single-asset ledger and writes the 128 KiB records prefix + # # to a file. Build it twice from the same source: + # # a) host-real -> native x86_64 binary -> host.records + # # b) riscv64 -> riscv64 binary, run via `qemu-riscv64` -> riscv.records + # # (The riscv64 crate currently builds a static archive; a small #[no_mangle] + # # entrypoint or an integration test cross-compiled to riscv64-unknown-linux-gnu + # # and executed under qemu is the mechanism to add.) + # # 2. Assert byte-for-byte equality: + # # cmp host.records riscv.records + # # A mismatch means the host can NOT soundly predict the machine ledger and + # # must fail CI hard. + # =========================================================================== diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a4f4381 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +While the crate is pre-1.0 (`0.0.x`), any release may contain breaking changes. + +## [Unreleased] + +### Added +- **Single-asset ledger API.** New bindings and `Ledger` wrapper support for the + single-asset `cma` ledger. +- **`host-real` feature.** Builds and links the real C++ `libcma` for the host + (x86_64) instead of the mock, for off-chain use (e.g. a sequencer predicting + the machine's ledger). Complements the existing `riscv64` cross-build path. +- Packaging metadata for crates.io / docs.rs: `LICENSE` (MIT), `rust-version` + (MSRV `1.74`), a `documentation` link, and `[package.metadata.docs.rs]` (an + offline `mock`-only docs build). Added `CHANGELOG.md`, `CONTRIBUTING.md`, + `SECURITY.md`, `deny.toml`, `rust-toolchain.toml`, and `rustfmt.toml`. + +### Changed +- **BREAKING: reshaped the ledger API around a single-asset ledger.** Removed + `LedgerMemoryMode` and reshaped `LedgerFileConfig`. Code that constructed a + ledger via the old memory-mode / file-config shape must be updated. +- **BREAKING: renamed the default mock feature `native` → `mock`.** The default + backend is now `mock`. Update any `--features native` usage accordingly. The + three mutually-exclusive backends are now `mock` (default), `host-real`, and + `riscv64`. + +### Fixed +- **Relocation safety for `cma_ledger_t`.** The self-referential C++ ledger is + now boxed so it is not moved after construction, preventing dangling internal + self-pointers. + +### Security / hardening +- Mutual-exclusion guards: enabling more than one backend feature now fails at + compile time (`compile_error!`) instead of silently letting the mock win in a + real build. +- `build.rs` verifies a checksum of the vendored/built libcma source. + +### Changed (breaking) +- **Migrated off the EOL `ethers-rs` onto `alloy`** (`alloy-primitives` + + `alloy-dyn-abi`). The public `Address` / `U256` types are now + `alloy_primitives::{Address, U256}` — a breaking change for downstream code + that used the ethers-typed API (hence the `0.0.1` → `0.1.0` bump). The ABI + parser (`parser.rs`) was reimplemented over `alloy-dyn-abi`; byte-for-byte + equivalence is pinned by the existing parser test vectors (all pass). + +### Known issues / tech debt +- **`json` (0.12)** — largely unmaintained (RUSTSEC-2022-0081). Planned + migration to `serde_json`. Surfaced by `deny.toml`. + +[Unreleased]: https://github.com/Mugen-Builders/libcma_binding_rust/commits/main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..60a1863 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +# Contributing + +Thanks for your interest in improving `libcma_binding_rust`. + +## Prerequisites + +Clone with submodules — the C headers live under `third_party/`: + +```bash +git clone --recurse-submodules https://github.com/Mugen-Builders/libcma_binding_rust +# or, if already cloned: +git submodule update --init --recursive +``` + +(`build.rs` will auto-init the submodules if they are missing, but doing it +yourself is more predictable.) + +## Building + +The default build uses the pure-Rust **`mock`** backend — no network or C++ +toolchain required: + +```bash +cargo build +``` + +A **real** libcma build links the compiled C++ library instead of the mock: + +```bash +# host (x86_64), off-chain use: +cargo build --no-default-features --features host-real + +# Cartesi machine target (riscv64): +cargo build --no-default-features --features riscv64 +``` + +Real builds additionally require **g++ ≥ 14**, GNU `make`, and **network access** +(`build.rs` fetches / compiles the archive from source). For `riscv64` you also +need the RISC-V GCC 14 cross toolchain (`g++-14-riscv64-linux-gnu`). + +### Feature rule: exactly one backend + +`mock`, `host-real`, and `riscv64` are **mutually exclusive** — exactly one must +be enabled, and a `compile_error!` guard enforces it. Because `mock` is a default +feature, selecting a real backend means also disabling defaults: + +```bash +cargo build --no-default-features --features host-real +``` + +Enabling a real backend without `--no-default-features` leaves `mock` on, which +the guard rejects. + +## Testing + +```bash +cargo test +``` + +Tests run against the `mock` backend by default. + +## Formatting and linting + +Before opening a pull request: + +```bash +cargo fmt --all +cargo clippy --all-targets +``` + +The repo pins `stable` via `rust-toolchain.toml` and ships a `rustfmt.toml`, so +formatting stays consistent across contributors. + +## Pull requests + +- Keep changes focused, and call out breaking changes clearly (the crate is + pre-1.0, so breaking changes are allowed but should be documented). +- Update `CHANGELOG.md` under the `## [Unreleased]` section. diff --git a/Cargo.lock b/Cargo.lock index 952dd0d..27d2353 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,415 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloy-dyn-abi" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a04eb4abc2b5074a18e687ee63918f407cc7990083cba9b999445f839796060" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-type-parser", + "alloy-sol-types", + "itoa", + "serde", + "serde_json", + "winnow 1.0.4", +] + +[[package]] +name = "alloy-json-abi" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cee30dd4c2f4b23f434fdf675e7bf9681b86768141277266c6f548ef25cba0a" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-primitives" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f007e257069855bdf21d27762fd3f3705a613f805c9a08309bf353503f081d71" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "fixed-cache", + "foldhash", + "hashbrown 0.17.1", + "indexmap 2.12.1", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.5", + "rapidhash", + "ruint", + "rustc-hash 2.1.3", + "secp256k1", + "serde", + "sha3", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24671b1f62edcf0f9b62994c7bf72cd621a04a4b99f5020ece1a647b40e2f103" +dependencies = [ + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5655c38d5f84955bf727b2eeb62fddd91ebb98fd1d7ae6eb77f73ea88f9b9cf" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6277c780e07b76951e09a59788dde230d1582612324177d11a43a61e21a6bb83" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.12.1", + "proc-macro-error3", + "proc-macro2", + "quote", + "sha3", + "syn 2.0.111", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9762b2ad3e5a0c09886de54fe549ab0056681df843cb082e2df7e1c0eb270d30" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.111", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da4c7130f0f01f4719678bda3db3bc7267fc2f7f9d0565e3bd964cd2bb45050d" +dependencies = [ + "serde", + "winnow 1.0.4", +] + +[[package]] +name = "alloy-sol-types" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96e74d6213180f78dbdccddce8af02a639c160c94b0a543fa35c77c58b8a7fc" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7a806ac6c8307b929df4645776290a50ee2aac754ad09d8bdf73391309e43af" +dependencies = [ + "ark-ff-asm 0.6.0", + "ark-ff-macros 0.6.0", + "ark-serialize 0.6.0", + "ark-std 0.6.0", + "digest 0.10.7", + "educe", + "num-bigint", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-ff-asm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1479009684adc073dff49a1025d3a7065b317a9ead25aaaca38cdc70058ba8a2" +dependencies = [ + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-ff-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0691ed21ef00ef89c1e9bda832eba493dda3ec2f8d892fb25b705f73f06bb8" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint", +] + +[[package]] +name = "ark-serialize" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74dd304fd536fb95d0a328e72be759209cc496a9da094c5bc56e5fea4f9e86b" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.6.0", + "digest 0.10.7", + "num-bigint", + "serde_with", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f153690697a2b91e5e1251ff98411ee5371500a111a0fd317a70e588eb300f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "ark-std" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "367c9c827ed431bff6868b7aa926e05b16eb46603cc8b6e768e4a5553fa1d155" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -36,9 +445,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base16ct" -version = "0.1.1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -55,7 +470,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -63,12 +478,48 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", - "shlex", + "rustc-hash 1.1.0", + "shlex 1.3.0", "syn 2.0.111", "which", ] +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207311705279250ba465076a1bac4b1ac982855fff73fc5f67e22158ac58cdc9" +dependencies = [ + "bitcoin-internals", + "hex-conservative 1.2.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", +] + [[package]] name = "bitflags" version = "2.10.0" @@ -96,6 +547,30 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "byte-slice-cast" version = "1.2.3" @@ -117,6 +592,16 @@ dependencies = [ "serde", ] +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex 2.0.1", +] + [[package]] name = "cexpr" version = "0.6.0" @@ -138,7 +623,10 @@ version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ + "iana-time-zone", "num-traits", + "serde", + "windows-link", ] [[package]] @@ -152,6 +640,18 @@ dependencies = [ "libloading", ] +[[package]] +name = "const-hex" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e2a781ebdf4467d1428dc4593067825fb646f6871475098d8577421af73558" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "proptest", + "serde_core", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -178,6 +678,21 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -187,6 +702,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -195,12 +719,12 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.4.9" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core", + "rand_core 0.6.4", "subtle", "zeroize", ] @@ -215,34 +739,75 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "der" -version = "0.6.1" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_more" -version = "1.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "1.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ + "convert_case", "proc-macro2", "quote", + "rustc_version 0.4.1", "syn 2.0.111", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", ] [[package]] @@ -251,21 +816,58 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "ecdsa" -version = "0.14.8" +version = "0.16.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" dependencies = [ "der", + "digest 0.10.7", "elliptic-curve", "rfc6979", "signature", + "spki", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -276,23 +878,43 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elliptic-curve" -version = "0.12.3" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", - "der", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", - "rand_core", + "pkcs8", + "rand_core 0.6.4", "sec1", "subtle", "zeroize", ] +[[package]] +name = "enum-ordinalize" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07f808d588c10e464ea6f7d3eaed500049eff30aaac103460f61828c2d65b3eb" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e528e2d34ba8a67a1a650b86beae8ef69fc5fdb638016f386b973226590432" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -310,87 +932,51 @@ dependencies = [ ] [[package]] -name = "ethabi" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" -dependencies = [ - "ethereum-types", - "hex", - "once_cell", - "regex", - "serde", - "serde_json", - "sha3", - "thiserror", - "uint", -] - -[[package]] -name = "ethbloom" -version = "0.13.0" +name = "fastrlp" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" dependencies = [ - "crunchy", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "scale-info", - "tiny-keccak", + "arrayvec", + "auto_impl", + "bytes", ] [[package]] -name = "ethereum-types" -version = "0.14.1" +name = "fastrlp" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" dependencies = [ - "ethbloom", - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "primitive-types", - "scale-info", - "uint", + "arrayvec", + "auto_impl", + "bytes", ] [[package]] -name = "ethers-core" -version = "1.0.2" +name = "ff" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade3e9c97727343984e1ceada4fdab11142d2ee3472d2c67027d56b1251d4f15" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "arrayvec", - "bytes", - "chrono", - "elliptic-curve", - "ethabi", - "generic-array", - "hex", - "k256", - "open-fastrlp", - "rand", - "rlp", - "rlp-derive", - "serde", - "serde_json", - "strum", - "thiserror", - "tiny-keccak", - "unicode-xid", + "rand_core 0.6.4", + "subtle", ] [[package]] -name = "ff" -version = "0.12.1" +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-cache" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +checksum = "2fe63500644ef0269fe6b744e7e5dc5c20b5eebf3d881bc2be53f194636f6583" dependencies = [ - "rand_core", - "subtle", + "equivalent", + "rapidhash", ] [[package]] @@ -400,17 +986,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand", + "rand 0.8.5", "rustc-hex", "static_assertions", ] +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "funty" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -419,6 +1035,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -432,6 +1049,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "glob" version = "0.3.3" @@ -440,26 +1069,43 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "group" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core", + "rand_core 0.6.4", "subtle", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -467,13 +1113,31 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35431185f361ccf3ffc58254628af5f1f5d5f28531da2e02e5d6c82bbc282a10" +dependencies = [ + "arrayvec", +] + [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -486,30 +1150,45 @@ dependencies = [ ] [[package]] -name = "impl-codec" -version = "0.6.0" +name = "hybrid-array" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ - "parity-scale-codec", + "typenum", ] [[package]] -name = "impl-rlp" -version = "0.3.0" +name = "iana-time-zone" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ - "rlp", + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", ] [[package]] -name = "impl-serde" -version = "0.4.0" +name = "iana-time-zone-haiku" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "serde", + "cc", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", ] [[package]] @@ -523,6 +1202,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -530,7 +1220,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", ] [[package]] @@ -542,12 +1243,32 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + [[package]] name = "json" version = "0.12.4" @@ -556,24 +1277,35 @@ checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" [[package]] name = "k256" -version = "0.11.6" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", + "once_cell", "sha2", - "sha3", ] [[package]] name = "keccak" -version = "0.1.5" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" dependencies = [ - "cpufeatures", + "cfg-if", + "cpufeatures 0.3.0", +] + +[[package]] +name = "keccak-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5dc2c0d691cbf7595cde551ced329cca99c2387c2cbc97754c5d0cd045d3ee" +dependencies = [ + "digest 0.10.7", + "sha3-asm", ] [[package]] @@ -596,10 +1328,11 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libcma_binding_rust" -version = "0.0.1" +version = "0.1.0" dependencies = [ + "alloy-dyn-abi", + "alloy-primitives", "bindgen", - "ethers-core", "hex", "json", "lazy_static", @@ -615,6 +1348,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -627,6 +1366,17 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "macro-string" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a9dbbfc75d2688ed057456ce8a3ee3f48d12eec09229f560f3643b9f275653" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "memchr" version = "2.7.6" @@ -650,45 +1400,46 @@ dependencies = [ ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "num-bigint" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" dependencies = [ - "autocfg", + "num-integer", + "num-traits", ] [[package]] -name = "once_cell" -version = "1.21.3" +name = "num-conv" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] -name = "open-fastrlp" -version = "0.1.4" +name = "num-integer" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "arrayvec", - "auto_impl", - "bytes", - "ethereum-types", - "open-fastrlp-derive", + "num-traits", ] [[package]] -name = "open-fastrlp-derive" -version = "0.1.1" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "bytes", - "proc-macro2", - "quote", - "syn 1.0.109", + "autocfg", + "libm", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "parity-scale-codec" version = "3.7.5" @@ -717,16 +1468,44 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pest" +version = "2.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -754,9 +1533,6 @@ checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ "fixed-hash", "impl-codec", - "impl-rlp", - "impl-serde", - "scale-info", "uint", ] @@ -769,6 +1545,28 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error-attr3" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82366fd7d8b7a440d66d13418820c69df9b3908bcb1a0476d7f5ce5d12f5a04d" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error3" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b511283ea8a74b4b39447b128c5d00f03a356b7424554b13e298a5550100d9ac" +dependencies = [ + "proc-macro-error-attr3", + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -778,6 +1576,21 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bitflags", + "num-traits", + "rand 0.9.5", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "unarray", +] + [[package]] name = "quote" version = "1.0.42" @@ -787,6 +1600,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "radium" version = "0.7.0" @@ -800,27 +1619,96 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rapidhash" +version = "4.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e" +dependencies = [ + "rustversion", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "ref-cast" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" dependencies = [ - "ppv-lite86", - "rand_core", + "ref-cast-impl", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "ref-cast-impl" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" dependencies = [ - "getrandom", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] @@ -854,13 +1742,12 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rfc6979" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "crypto-bigint", "hmac", - "zeroize", + "subtle", ] [[package]] @@ -874,28 +1761,76 @@ dependencies = [ ] [[package]] -name = "rlp-derive" -version = "0.1.0" +name = "ruint" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +checksum = "45caf26f647c19115bf9c453c70ffe4a4a3a6390dceebd942610584f99b8ddce" dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "ark-ff 0.6.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.5", + "rand 0.9.5", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", ] +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + [[package]] name = "rustc-hex" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + [[package]] name = "rustix" version = "0.38.44" @@ -922,34 +1857,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] -name = "scale-info" -version = "2.11.6" +name = "schemars" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" dependencies = [ - "cfg-if", - "derive_more", - "parity-scale-codec", - "scale-info-derive", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] -name = "scale-info-derive" -version = "2.11.6" +name = "schemars" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", + "dyn-clone", + "ref-cast", + "serde", + "serde_json", ] [[package]] name = "sec1" -version = "0.3.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", "der", @@ -959,6 +1894,50 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secp256k1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" +dependencies = [ + "bitcoin_hashes", + "rand 0.9.5", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + [[package]] name = "serde" version = "1.0.228" @@ -1002,6 +1981,25 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.1", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "time", +] + [[package]] name = "sha2" version = "0.10.9" @@ -1009,41 +2007,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] name = "sha3" -version = "0.10.8" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ - "digest", + "digest 0.11.3", "keccak", ] +[[package]] +name = "sha3-asm" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6287fd675f713484342a89cbf0a386abef5f15919cfad607e5e1f19e1e15331" +dependencies = [ + "cc", + "cfg-if", +] + [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signature" -version = "1.6.4" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "spki" -version = "0.6.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -1056,38 +2076,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "strum" -version = "0.24.1" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros", -] +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "strum_macros" -version = "0.24.3" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "heck", "proc-macro2", "quote", - "rustversion", - "syn 1.0.109", + "unicode-ident", ] [[package]] -name = "subtle" -version = "2.6.1" +name = "syn" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] name = "syn" -version = "1.0.109" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ "proc-macro2", "quote", @@ -1095,14 +2115,15 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.111" +name = "syn-solidity" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "083be3061e64d362cbe6ef12cfe1307ba3884326d8856448fe8a120fa2c44ebf" dependencies = [ + "paste", "proc-macro2", "quote", - "unicode-ident", + "syn 2.0.111", ] [[package]] @@ -1112,34 +2133,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] -name = "thiserror" -version = "1.0.69" +name = "time" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ - "thiserror-impl", + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", ] [[package]] -name = "thiserror-impl" -version = "1.0.69" +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "num-conv", + "time-core", ] [[package]] -name = "tiny-keccak" -version = "2.0.2" +name = "tinyvec" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ - "crunchy", + "tinyvec_macros", ] +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "toml_datetime" version = "0.7.3" @@ -1155,10 +2192,10 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap", + "indexmap 2.12.1", "toml_datetime", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] @@ -1167,14 +2204,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "winnow", + "winnow 0.7.14", ] [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uint" @@ -1188,18 +2231,36 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -1212,6 +2273,60 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + [[package]] name = "which" version = "4.4.2" @@ -1224,12 +2339,65 @@ dependencies = [ "rustix", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1321,6 +2489,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wyz" version = "0.5.1" @@ -1355,3 +2538,17 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] diff --git a/Cargo.toml b/Cargo.toml index eb88f85..acc7f8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,15 @@ [package] name = "libcma_binding_rust" -version = "0.0.1" +version = "0.1.0" edition = "2021" -authors = ["Idogwu Chinonso idogwuchi@gmail.com"] -description = "Rust utilities for Cartesi applicaitons, functions as a plugin wallet lib for managing applicaiton assets and also a parser for encoding and decoding vouchers and applicaion inputs." +# Minimum Supported Rust Version. Conservative, plausible floor for edition 2021 +# plus the crate's dependencies; verified to build with the current toolchain. +rust-version = "1.74" +authors = ["Idogwu Chinonso "] +description = "Rust utilities for Cartesi applications: a plugin wallet library for managing application assets, plus a parser for encoding and decoding vouchers and application inputs." license = "MIT" repository = "https://github.com/Mugen-Builders/libcma_binding_rust" +documentation = "https://docs.rs/libcma_binding_rust" readme = "README.md" keywords = ["cartesi", "rollup", "ledger", "parser", "blockchain"] categories = ["api-bindings", "cryptography", "blockchain"] @@ -17,15 +21,34 @@ path = "src/lib.rs" [dependencies] hex = "0.4" lazy_static = { version = "1.4", optional = true } +# NOTE: `json` (0.12) is largely unmaintained. Tech debt: migrate to `serde_json` +# (the de-facto standard). Tracked in CHANGELOG.md and surfaced by deny.toml. json = "0.12" -ethers-core = "1.0.0" +# Ethereum primitives + dynamic ABI. Migrated off the EOL `ethers-rs` onto `alloy` +# (aligned with the `alloy-primitives`/`alloy-dyn-abi` 1.6.x the downstream sequencer resolves). +alloy-primitives = "1.6" +alloy-dyn-abi = "1.6" [build-dependencies] bindgen = "0.69" +# docs.rs builds the crate OFFLINE (no network, no C++ toolchain), so it must use +# the `mock` backend — a pure-Rust stub that needs neither. `host-real`/`riscv64` +# would try to fetch/compile the real libcma and fail on the docs.rs sandbox. +[package.metadata.docs.rs] +features = ["mock"] +default-target = "x86_64-unknown-linux-gnu" + [features] -default = ["native"] -native = ["lazy_static"] # Mac testing with mocks +# Exactly ONE backend must be enabled — they are mutually exclusive (enforced by the +# compile_error! guard at the top of src/lib.rs). `mock` is the default so `cargo build` +# and `cargo test` work out of the box, but it is a STUB ledger, NOT real libcma. +# +# IMPORTANT for consumers: selecting a REAL backend requires ALSO disabling defaults, +# otherwise `mock` stays enabled and silently wins (a fake ledger in production): +# default-features = false, features = ["host-real"] # (or "riscv64") +default = ["mock"] +mock = ["lazy_static"] # In-memory MOCK ledger (thread-local stubs in src/mocks.rs) — plumbing/tests only, NOT real libcma riscv64 = [] # Cross-build the real C++ libcma for the Cartesi machine (riscv64) # Build the real C++ libcma for the HOST (x86_64) and link it instead of the mock. # Used off-chain (e.g. by a sequencer predicting the machine's ledger): the DEFS in diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c2cb1cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Idogwu Chinonso + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 7edcd2a..8ed3e92 100644 --- a/README.md +++ b/README.md @@ -45,15 +45,59 @@ The Cartesi SDK / app Docker image used to build the machine already provides al ## Feature flags -| Feature | Default | Purpose | -| --------- | ------- | ------- | -| `native` | yes | Compiles `src/mocks.rs` shims so host tests run without the RISC-V `libcma` archive | -| `riscv64` | no | Cross-build path that links the real C++ `cma` library; `build.rs` cross-compiles `build/riscv64/libcma.a` from the submodule source if it isn't already present (needs the RISC-V GCC 14 cross toolchain) | +The crate has **three mutually exclusive backends**. Exactly one must be enabled; +enabling zero or more than one is a hard `compile_error!` (enforced at the top of +`src/lib.rs`). + +| Feature | Default | Real libcma? | When to use | +| ----------- | ------- | ------------------------ | ----------- | +| `mock` | yes | No — in-memory **stub** | Host development and `cargo test`: compiles the thread-local stubs in `src/mocks.rs` so the crate builds and the plumbing/parser tests run with no C++ toolchain, no network, and no RISC-V archive. **Never use in production — it is not a real ledger.** | +| `host-real` | no | Yes (host, x86_64) | Running the **real** C++ `libcma` off-chain on the host, e.g. a sequencer predicting the Cartesi machine's ledger state. `build.rs` builds and links the real static archive for the host. | +| `riscv64` | no | Yes (Cartesi machine) | Running the **real** C++ `libcma` **inside** the Cartesi machine. `build.rs` cross-compiles `build/riscv64/libcma.a` from the submodule source if it isn't already present (needs the RISC-V GCC 14 cross toolchain). | + +### Selecting a real backend (important footgun) + +The backends are mutually exclusive **and** `mock` is a default feature, so the +link gate in `build.rs` keys off `mock`. To build against the real `libcma` you +MUST also turn default features off — otherwise the default `mock` stays enabled +and you silently link the stub instead of the real ledger: ```bash +# real libcma on the host (off-chain, e.g. sequencer prediction) +cargo build --no-default-features --features host-real + +# real libcma cross-compiled for the Cartesi machine cargo build --no-default-features --features riscv64 ``` +In `Cargo.toml`: + +```toml +libcma_binding_rust = { version = "...", default-features = false, features = ["host-real"] } # or "riscv64" +``` + +If you forget `default-features = false`, enabling `host-real` or `riscv64` +alongside the default `mock` trips the mutual-exclusivity `compile_error!` — read +its message; the fix is to disable default features. + +### Determinism / reproducibility + +`host-real` and `riscv64` compile the C++ `libcma` with SIMD-free / generic flags +(`-DBOOST_UNORDERED_DISABLE_SSE2`, `-DBOOST_UNORDERED_DISABLE_NEON`, +`-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION`). This makes the on-disk 32-byte +account records (`balance` u64 little-endian | `owner` 20 bytes | 4 bytes +padding) **byte-identical** across x86_64 and riscv64. That invariant is what +makes off-chain prediction with `host-real` sound: the host reproduces, byte for +byte, exactly what the machine computes on-chain. + +### Thread safety + +`Ledger` wraps a self-referential C++ object (Boost.Interprocess) held on the +heap for relocation safety, and is therefore **`!Send` / `!Sync`**. Do not move +or share a `Ledger` across threads without external synchronization. Downstream +code that needs `Send` typically wraps the `Ledger` in a mutex together with its +own `unsafe impl Send`. + ## Ledger wrapper `Ledger` wraps `cma_ledger_*` with helpers for file/buffer initialization, asset/account retrieval, deposit/withdraw/transfer, balance, and total supply. @@ -93,9 +137,9 @@ cargo test - `tests/parser_tests.rs` — integration tests against the pure-Rust parser - `tests/parser_vectors.rs` — vectors ported from `third_party/machine-asset-tools/tests/parser.c` -- `tests/ledger_tests.rs` — ledger tests via native mocks +- `tests/ledger_tests.rs` — ledger tests via the `mock` backend -CI runs native tests on every push/PR and attempts an riscv64 link check when `libcma` can be built. +CI runs the `mock`-backend tests on every push/PR and attempts an riscv64 link check when `libcma` can be built. ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..fe7fb4f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Supported versions + +The crate is pre-1.0 and under active development. Only the latest published +`0.0.x` release receives security fixes. + +| Version | Supported | +| ---------------- | ------------------ | +| 0.0.x (latest) | :white_check_mark: | +| older 0.0.x | :x: | + +## Reporting a vulnerability + +Please report security vulnerabilities **privately** — do not open a public +issue or pull request. + +- Preferred: open a private advisory via GitHub Security Advisories on the + [repository](https://github.com/Mugen-Builders/libcma_binding_rust/security/advisories/new). +- Alternatively, email the maintainer: **idogwuchi@gmail.com**. + +Please include a description, the affected version(s), and reproduction steps +where possible. You can expect an initial acknowledgement within a reasonable +time frame; we will then coordinate a fix and a disclosure timeline with you. + +## Scope note + +The default `mock` backend is an in-memory stub for development and testing — it +is **not** the real libcma ledger and must not be used to custody real assets in +production. Use the `host-real` or `riscv64` backends for real deployments. diff --git a/STRUCTURE.md b/STRUCTURE.md index b87b8cd..c11008b 100644 --- a/STRUCTURE.md +++ b/STRUCTURE.md @@ -20,7 +20,7 @@ libcma_binding_rust/ # crate root (see Cargo.toml [package] name) │ ├── ledger.rs # Ledger wrapper + file/buffer init configs │ ├── parser.rs # High-level parser / voucher helpers │ ├── helpers.rs # Shared helpers -│ └── mocks.rs # #[cfg(feature = "native")] C ABI shims for tests +│ └── mocks.rs # #[cfg(feature = "mock")] C ABI stub shims for tests ├── tests/ │ ├── ledger_tests.rs # Ledger behavior (mock-backed by default) │ └── parser_tests.rs # Parser / encoding tests @@ -47,12 +47,15 @@ There is **no** checked-in `lib/cpp-build` tree in this layout: bindgen runs aga - `third_party/machine-guest-tools/sys-utils/libcmt/include` 2. **Header root** — `wrapper.h` at the crate root. 3. **Generated output** — `$OUT_DIR/bindings.rs` (included from `src/lib.rs` inside the `bindings` module). -4. **Linking** — If the **`native` feature is disabled**, `build.rs` adds `-L third_party/machine-asset-tools/build/riscv64` and links `static=cma`. With **`native` enabled (default)**, it does not link that archive; `src/mocks.rs` supplies compatible `#[no_mangle]` symbols for development and `cargo test` on the host. +4. **Linking** — The link gate keys off the **`mock`** feature. With **`mock` enabled (default)**, `build.rs` links no C++ archive; `src/mocks.rs` supplies compatible `#[no_mangle]` stub symbols for development and `cargo test` on the host. With a **real backend** — `host-real` (host x86_64) or `riscv64` (Cartesi machine), each requiring `--no-default-features` — `build.rs` builds and links the static `cma` archive instead of the mock (for `riscv64` it adds `-L third_party/machine-asset-tools/build/riscv64` and links `static=cma`). ## Feature flags -- **`native` (default)** — Compiles `mocks.rs`. Intended for host builds and unit/integration tests without the RISC-V static library. -- **`riscv64`** — Placeholder feature for cross-compilation workflows; default build still keys off “not native” for linking. +Three **mutually exclusive** backends; exactly one must be enabled (a `compile_error!` in `src/lib.rs` enforces this). Selecting a real backend requires `default-features = false` — otherwise the default `mock` stays on and silently wins. + +- **`mock` (default)** — Compiles `src/mocks.rs`: an in-memory **stub** ledger for host builds and unit/integration tests without any C++ toolchain or the RISC-V static library. Not real libcma; never for production. +- **`host-real`** — Builds and links the **real** C++ `libcma` for the host (x86_64). Used off-chain, e.g. a sequencer predicting the machine's ledger. Built SIMD-free so its 32-byte account records are byte-identical to the `riscv64` build. +- **`riscv64`** — Cross-compiles and links the **real** C++ `libcma` for the Cartesi machine (riscv64). ## Public surface (`src/lib.rs`) @@ -64,7 +67,7 @@ Re-exports include: ## Tests -- **`tests/ledger_tests.rs`** — Ledger API; uses mock implementations when `native` is on. +- **`tests/ledger_tests.rs`** — Ledger API; uses mock implementations when the `mock` backend (default) is on. - **`tests/parser_tests.rs`** — Parser and voucher-related coverage. Run: `cargo test`. diff --git a/build.rs b/build.rs index a97a9df..315d583 100644 --- a/build.rs +++ b/build.rs @@ -1,5 +1,11 @@ use std::{env, path::Path, path::PathBuf, process::Command}; +/// Pinned SHA-256 of the nlohmann/json v3.12.0 single-header release asset (`json.hpp`). +/// Verified out-of-band against the upstream GitHub release download. A mismatch means the +/// fetched header was corrupted or tampered with — the build must refuse to proceed. +const NLOHMANN_JSON_SHA256: &str = + "aaf127c04cb31c406e5b04a63f1ae89369fccde6d8fa7cdda1ed4f32dfc5de63"; + fn main() { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); @@ -10,7 +16,11 @@ fn main() { // make the include-path lookups below fail. Pull the submodules in automatically so the crate // builds with nothing more than `cargo build` — no out-of-band setup step required. if !mat.join("include").exists() { - run("git", &["submodule", "update", "--init", "--recursive"], &manifest_dir); + run( + "git", + &["submodule", "update", "--init", "--recursive"], + &manifest_dir, + ); } let cma_include_dir = mat.join("include").canonicalize().expect( @@ -56,12 +66,36 @@ fn main() { .write_to_file(out_dir.join("bindings.rs")) .expect("Failed to write bindings"); - // Link the real C++ libcma when not using the native mock. Two targets: - // - `riscv64` → cross-compile for the Cartesi machine (default when non-native). + // MOCK backend active: shout about it so a fake in-memory ledger can never be shipped to + // production unnoticed. NOTE also that everything in the `!mock` block below (the wget of + // nlohmann/json, `make third-party` which fetches Boost et al., and the C++ compile of + // libcma.a) is gated OFF here — so a `mock` build downloads no third-party C++ sources and + // needs no C++ toolchain. + if cfg!(feature = "mock") { + println!( + "cargo:warning=libcma_binding_rust: building with the MOCK ledger (feature `mock`) — \ + this is NOT real libcma; never use in production. For a real build use \ + default-features = false, features = [\"host-real\"] (or \"riscv64\")." + ); + } + + // Link the real C++ libcma when the MOCK is NOT selected. Two targets: + // - `riscv64` → cross-compile for the Cartesi machine (the non-mock default). // - `host-real` → compile for the host (x86_64), so an off-chain consumer runs the // *same* ledger the machine will. The DEFS in machine-asset-tools force // SIMD-free/generic paths so the record bytes match across arches. - if !cfg!(feature = "native") { + // + // Everything inside this block fetches third-party C++ sources from the NETWORK and invokes a + // C++ COMPILER; none of it runs for a `mock` build. Keeping the fetch/compile confined here is + // what keeps the default `mock` path hermetic (no network, no toolchain, docs.rs/offline-safe). + if !cfg!(feature = "mock") { + // Real builds are NOT hermetic: they require network access and a C++ toolchain (g++ >= 14 + // for C++20/23). Make that requirement visible in the build log up front. + println!( + "cargo:warning=libcma_binding_rust: building REAL libcma from C++ source — this build \ + requires network access and a C++ toolchain (g++ >= 14). See build.rs for details." + ); + let host = cfg!(feature = "host-real"); // Distinct object dirs so a host build and a cross build never clobber each other. let obj_subdir = if host { "build/host" } else { "build/riscv64" }; @@ -87,8 +121,10 @@ fn main() { } else { ( "riscv64-linux-gnu-".to_string(), - env::var("CMA_RISCV64_CXX").unwrap_or_else(|_| "riscv64-linux-gnu-g++-14".into()), - env::var("CMA_RISCV64_CC").unwrap_or_else(|_| "riscv64-linux-gnu-gcc-14".into()), + env::var("CMA_RISCV64_CXX") + .unwrap_or_else(|_| "riscv64-linux-gnu-g++-14".into()), + env::var("CMA_RISCV64_CC") + .unwrap_or_else(|_| "riscv64-linux-gnu-gcc-14".into()), "riscv64-linux-gnu-ar".to_string(), ) }; @@ -98,6 +134,8 @@ fn main() { let nlohmann = mat.join("third-party/nlohmann/json.hpp"); if !nlohmann.exists() { std::fs::create_dir_all(mat.join("third-party/nlohmann")).ok(); + // GNU `wget` has no `--checksum` flag (only wget2 does), so we download here and + // verify the SHA-256 against a pinned constant below rather than at fetch time. run( "wget", &[ @@ -108,11 +146,22 @@ fn main() { &mat, ); } + // Supply-chain gate: pin + verify the one header the Makefile does not fetch itself. + // This runs on BOTH a fresh download and a pre-existing/vendored copy, so a tampered or + // corrupted cache is caught too. A mismatch is a hard, un-ignorable build failure. + verify_sha256(&nlohmann, NLOHMANN_JSON_SHA256); // Download + stage the third-party deps, then compile the static archive. The Makefile // hardcodes `libcma_OBJDIR := build/riscv64`; override it so the host build lands in its // own dir (command-line assignments beat the Makefile's `:=`). - run("make", &["third-party", &format!("TOOLCHAIN_PREFIX={toolchain_prefix}")], &mat); + run( + "make", + &[ + "third-party", + &format!("TOOLCHAIN_PREFIX={toolchain_prefix}"), + ], + &mat, + ); run( "make", &[ @@ -152,7 +201,10 @@ fn main() { /// Used as a fallback so bindgen works even when libclang ships without its own resource headers. fn gcc_builtin_include() -> Option { let cc = env::var("CC").unwrap_or_else(|_| "cc".into()); - let out = Command::new(cc).arg("-print-file-name=include").output().ok()?; + let out = Command::new(cc) + .arg("-print-file-name=include") + .output() + .ok()?; if !out.status.success() { return None; } @@ -164,6 +216,55 @@ fn gcc_builtin_include() -> Option { } } +/// Verify a file against a pinned SHA-256, failing the build loudly on mismatch. Used as the +/// supply-chain gate for the nlohmann/json header fetched over the network. Shells out to a +/// system hashing tool so no extra crate dependency is required. +fn verify_sha256(path: &Path, expected: &str) { + let actual = sha256_of(path).unwrap_or_else(|| { + panic!( + "cannot verify {}: no usable SHA-256 tool found (need `sha256sum`, `shasum`, or \ + `openssl` on PATH). Refusing to build against an unverified nlohmann/json header.", + path.display() + ) + }); + assert!( + actual.eq_ignore_ascii_case(expected), + "SHA-256 mismatch for {}: expected {expected}, got {actual}. Refusing to build against an \ + unverified nlohmann/json header (possible supply-chain tampering or a corrupted download). \ + Delete the file and re-fetch, or update the pin in build.rs if the upstream release changed.", + path.display() + ); +} + +/// Compute the lowercase hex SHA-256 of `path` using whatever system tool is available +/// (`sha256sum`, then `shasum -a 256`, then `openssl dgst -sha256 -r`). Returns None if none +/// produced a valid 64-char hex digest. Avoids pulling in a hashing crate as a build-dependency. +fn sha256_of(path: &Path) -> Option { + let p = path.to_str()?; + // (command, args preceding the file path); each tool prints the digest as the first token. + let candidates: [(&str, &[&str]); 3] = [ + ("sha256sum", &[]), + ("shasum", &["-a", "256"]), + ("openssl", &["dgst", "-sha256", "-r"]), + ]; + for (cmd, pre) in candidates { + let mut args: Vec<&str> = pre.to_vec(); + args.push(p); + if let Ok(out) = Command::new(cmd).args(&args).output() { + if out.status.success() { + if let Ok(s) = String::from_utf8(out.stdout) { + if let Some(tok) = s.split_whitespace().next() { + if tok.len() == 64 && tok.bytes().all(|b| b.is_ascii_hexdigit()) { + return Some(tok.to_ascii_lowercase()); + } + } + } + } + } + } + None +} + /// Run a command in `cwd`, panicking with a helpful message if it is missing or fails. fn run(cmd: &str, args: &[&str], cwd: &Path) { let status = Command::new(cmd) diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..9ff5aa4 --- /dev/null +++ b/deny.toml @@ -0,0 +1,65 @@ +# cargo-deny configuration — https://embarkstudios.github.io/cargo-deny/ +# +# Run: cargo deny check +# +# Purpose here is twofold: +# 1. Enforce a permissive-only license allow-list across the dependency tree. +# 2. Surface this crate's known tech debt — the deprecated `json` and +# `ethers-core` dependencies — via security-advisory and ban checks, as a +# standing reminder to migrate to `serde_json` and `alloy` respectively. + +[graph] +all-features = false + +# --------------------------------------------------------------------------- +# Security advisories (RustSec DB). +# --------------------------------------------------------------------------- +[advisories] +version = 2 +# Vulnerabilities are always denied under schema v2. +# Check every crate (direct + transitive) for `unmaintained` advisories so +# `json` (RUSTSEC-2022-0081) and any ethers-rs advisory are flagged. +unmaintained = "all" +yanked = "warn" +ignore = [] + +# --------------------------------------------------------------------------- +# License policy — permissive licenses only. +# --------------------------------------------------------------------------- +[licenses] +version = 2 +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "Unicode-DFS-2016", + "Unicode-3.0", + "CC0-1.0", + "Unlicense", + "MPL-2.0", +] +confidence-threshold = 0.8 + +# --------------------------------------------------------------------------- +# Bans — explicitly flag the deprecated direct dependencies. +# --------------------------------------------------------------------------- +[bans] +multiple-versions = "warn" +wildcards = "warn" +# These are intentionally listed so `cargo deny check bans` surfaces them as a +# migration reminder. Remove each entry once the corresponding replacement lands. +deny = [ + { name = "json", reason = "Unmaintained (RUSTSEC-2022-0081); migrate to serde_json." }, + # ethers-core removed — the crate migrated to alloy (alloy-primitives / alloy-dyn-abi). +] + +# --------------------------------------------------------------------------- +# Source registries. +# --------------------------------------------------------------------------- +[sources] +unknown-registry = "warn" +unknown-git = "warn" diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..73cb934 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..f42c8b3 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +edition = "2021" +max_width = 100 diff --git a/src/error.rs b/src/error.rs index cd4a152..ecef7f3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,19 +28,43 @@ impl LedgerError { x if x == bindings::CMA_LEDGER_SUCCESS as i32 => unreachable!(), x if x == bindings::CMA_LEDGER_ERROR_UNKNOWN as i32 => LedgerError::Unknown, x if x == bindings::CMA_LEDGER_ERROR_EXCEPTION as i32 => LedgerError::Exception, - x if x == bindings::CMA_LEDGER_ERROR_INSUFFICIENT_FUNDS as i32 => LedgerError::InsufficientFunds, - x if x == bindings::CMA_LEDGER_ERROR_ACCOUNT_NOT_FOUND as i32 => LedgerError::AccountNotFound, - x if x == bindings::CMA_LEDGER_ERROR_ASSET_NOT_FOUND as i32 => LedgerError::AssetNotFound, - x if x == bindings::CMA_LEDGER_ERROR_BALANCE_NOT_FOUND as i32 => LedgerError::BalanceNotFound, - x if x == bindings::CMA_LEDGER_ERROR_SUPPLY_OVERFLOW as i32 => LedgerError::SupplyOverflow, - x if x == bindings::CMA_LEDGER_ERROR_BALANCE_OVERFLOW as i32 => LedgerError::BalanceOverflow, - x if x == bindings::CMA_LEDGER_ERROR_INVALID_ACCOUNT as i32 => LedgerError::InvalidAccount, - x if x == bindings::CMA_LEDGER_ERROR_INSERTION_ERROR as i32 => LedgerError::InsertionError, - x if x == bindings::CMA_LEDGER_ERROR_MAX_ASSETS_REACHED as i32 => LedgerError::MaxAssetsReached, - x if x == bindings::CMA_LEDGER_ERROR_MAX_ACCOUNTS_REACHED as i32 => LedgerError::MaxAccountsReached, - x if x == bindings::CMA_LEDGER_ERROR_MAX_BALANCES_REACHED as i32 => LedgerError::MaxBalancesReached, + x if x == bindings::CMA_LEDGER_ERROR_INSUFFICIENT_FUNDS as i32 => { + LedgerError::InsufficientFunds + } + x if x == bindings::CMA_LEDGER_ERROR_ACCOUNT_NOT_FOUND as i32 => { + LedgerError::AccountNotFound + } + x if x == bindings::CMA_LEDGER_ERROR_ASSET_NOT_FOUND as i32 => { + LedgerError::AssetNotFound + } + x if x == bindings::CMA_LEDGER_ERROR_BALANCE_NOT_FOUND as i32 => { + LedgerError::BalanceNotFound + } + x if x == bindings::CMA_LEDGER_ERROR_SUPPLY_OVERFLOW as i32 => { + LedgerError::SupplyOverflow + } + x if x == bindings::CMA_LEDGER_ERROR_BALANCE_OVERFLOW as i32 => { + LedgerError::BalanceOverflow + } + x if x == bindings::CMA_LEDGER_ERROR_INVALID_ACCOUNT as i32 => { + LedgerError::InvalidAccount + } + x if x == bindings::CMA_LEDGER_ERROR_INSERTION_ERROR as i32 => { + LedgerError::InsertionError + } + x if x == bindings::CMA_LEDGER_ERROR_MAX_ASSETS_REACHED as i32 => { + LedgerError::MaxAssetsReached + } + x if x == bindings::CMA_LEDGER_ERROR_MAX_ACCOUNTS_REACHED as i32 => { + LedgerError::MaxAccountsReached + } + x if x == bindings::CMA_LEDGER_ERROR_MAX_BALANCES_REACHED as i32 => { + LedgerError::MaxBalancesReached + } x if x == bindings::CMA_LEDGER_ERROR_ASSET_SUPPLY as i32 => LedgerError::AssetSupply, - x if x == bindings::CMA_LEDGER_ERROR_ACCOUNT_BALANCE as i32 => LedgerError::AccountBalance, + x if x == bindings::CMA_LEDGER_ERROR_ACCOUNT_BALANCE as i32 => { + LedgerError::AccountBalance + } x if x == bindings::CMA_LEDGER_ERROR_REMOVE as i32 => LedgerError::Remove, _ => LedgerError::Other(code), } @@ -52,9 +76,7 @@ impl LedgerError { if msg.is_null() { format!("{:?}", self) } else { - std::ffi::CStr::from_ptr(msg) - .to_string_lossy() - .to_string() + std::ffi::CStr::from_ptr(msg).to_string_lossy().to_string() } } } @@ -85,9 +107,15 @@ impl ParserError { x if x == bindings::CMA_PARSER_SUCCESS as i32 => unreachable!(), x if x == bindings::CMA_PARSER_ERROR_UNKNOWN as i32 => ParserError::Unknown, x if x == bindings::CMA_PARSER_ERROR_EXCEPTION as i32 => ParserError::Exception, - x if x == bindings::CMA_PARSER_ERROR_INCOMPATIBLE_INPUT as i32 => ParserError::IncompatibleInput, - x if x == bindings::CMA_PARSER_ERROR_MALFORMED_INPUT as i32 => ParserError::MalformedInput, - x if x == bindings::CMA_PARSER_ERROR_INVALID_AMOUNT as i32 => ParserError::InvalidAmount, + x if x == bindings::CMA_PARSER_ERROR_INCOMPATIBLE_INPUT as i32 => { + ParserError::IncompatibleInput + } + x if x == bindings::CMA_PARSER_ERROR_MALFORMED_INPUT as i32 => { + ParserError::MalformedInput + } + x if x == bindings::CMA_PARSER_ERROR_INVALID_AMOUNT as i32 => { + ParserError::InvalidAmount + } _ => ParserError::Other(code), } } @@ -98,9 +126,7 @@ impl ParserError { if msg.is_null() { format!("{:?}", self) } else { - std::ffi::CStr::from_ptr(msg) - .to_string_lossy() - .to_string() + std::ffi::CStr::from_ptr(msg).to_string_lossy().to_string() } } } @@ -112,4 +138,4 @@ impl std::fmt::Display for ParserError { } } -impl std::error::Error for ParserError {} \ No newline at end of file +impl std::error::Error for ParserError {} diff --git a/src/helpers.rs b/src/helpers.rs index 0a87a3b..f4288f4 100644 --- a/src/helpers.rs +++ b/src/helpers.rs @@ -1,7 +1,7 @@ -use hex; -use ethers_core::types::{Address}; -use json::{JsonValue, object}; use crate::parser::CmaVoucher; +use alloy_primitives::Address; +use hex; +use json::{object, JsonValue}; pub fn hex_to_string(hex: &str) -> Result> { let hexstr = hex.strip_prefix("0x").unwrap_or(hex); @@ -40,4 +40,4 @@ impl ToJson for CmaVoucher { "payload" => format!("{}", self.payload), } } -} \ No newline at end of file +} diff --git a/src/ledger.rs b/src/ledger.rs index 711a1bf..75ecbc3 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -1,3 +1,28 @@ +//! Safe Rust wrapper around the C++ `libcma` ledger. +//! +//! # Backends +//! +//! Exactly one backend feature is compiled in (they are mutually exclusive; see +//! the `compile_error!` guards in `lib.rs`): +//! +//! - **`mock`** (default) — an in-memory **stub** ledger (`src/mocks.rs`). Needs +//! no C++ toolchain or network; for compile/plumbing tests only. It is **not** +//! real libcma and must never be used in production. +//! - **`host-real`** — the real C++ libcma built for the host (x86_64), used +//! off-chain (e.g. a sequencer predicting the Cartesi machine's ledger). +//! - **`riscv64`** — the real C++ libcma cross-compiled to run inside the +//! Cartesi machine. +//! +//! # Reproducibility invariant +//! +//! `host-real` and `riscv64` build libcma with SIMD-free / generic flags +//! (`-DBOOST_UNORDERED_DISABLE_SSE2`, `-DBOOST_UNORDERED_DISABLE_NEON`, +//! `-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION`) so the on-disk 32-byte account +//! records (`balance` u64 little-endian | `owner` 20 bytes | 4 bytes padding) are +//! byte-identical across x86_64 and riscv64. This is what makes off-chain +//! prediction with `host-real` sound: the host reproduces, byte for byte, +//! exactly what the machine computes on-chain. + use crate::bindings; use crate::error::LedgerError; use crate::types::*; @@ -46,7 +71,12 @@ pub enum LedgerAsset { impl LedgerAsset { /// Lower to the C `(asset_type, token_address)` pair. The address is returned /// by value so the caller can keep it alive while passing a pointer to it. - fn to_c(self) -> (bindings::cma_ledger_asset_type_t, Option) { + fn to_c( + self, + ) -> ( + bindings::cma_ledger_asset_type_t, + Option, + ) { match self { LedgerAsset::Ether => ( bindings::cma_ledger_asset_type_t_CMA_LEDGER_ASSET_TYPE_BASE, @@ -100,7 +130,17 @@ impl Default for LedgerBufferConfig { } } -/// Safe wrapper around the C ledger +/// Safe wrapper around the C++ `libcma` ledger. +/// +/// # Thread safety +/// +/// `Ledger` wraps a self-referential C++ object (Boost.Interprocess): the +/// backend caches an internal reference bound to its own storage, so the object +/// is heap-pinned via [`Box`] to keep its address stable across moves. As a +/// consequence `Ledger` is **`!Send`** and **`!Sync`** — it must not be moved or +/// shared across threads without external synchronization. Downstream code that +/// needs `Send` typically wraps the `Ledger` in a mutex together with its own +/// `unsafe impl Send`. pub struct Ledger { // Boxed so the C++ ledger object has a STABLE heap address. The backends // (`cma_ledger_memory`, `cma_ledger_single`) are self-referential — they cache @@ -406,7 +446,7 @@ impl Ledger { } } - /// Get total supply for an asset (via [`cma_ledger_retrieve_asset`] with find). + /// Get total supply for an asset (via `cma_ledger_retrieve_asset` with find). pub fn get_total_supply(&self, asset_id: LedgerAssetId) -> Result { unsafe { let mut asset_id_mut = asset_id.0; diff --git a/src/lib.rs b/src/lib.rs index 44578d5..99dc93c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,71 @@ +//! Rust bindings for Cartesi's `libcma` (Cartesi Machine Application tooling): +//! parse rollup inputs, build on-chain voucher payloads, and manage a ledger of +//! application assets (ETH, ERC-20, ERC-721, ERC-1155). +//! +//! # Backends +//! +//! Exactly one backend feature is compiled in — they are **mutually exclusive**, +//! enforced by the `compile_error!` guards below: +//! +//! - **`mock`** (default) — an in-memory **stub** ledger (`src/mocks.rs`). Needs +//! no C++ toolchain, network, or RISC-V archive; for compile/plumbing tests +//! only. It is **not** real libcma and must never be used in production. +//! - **`host-real`** — the real C++ libcma built for the host (x86_64). Used +//! off-chain, e.g. a sequencer predicting the Cartesi machine's ledger. +//! - **`riscv64`** — the real C++ libcma cross-compiled to run inside the +//! Cartesi machine. +//! +//! Selecting a real backend requires disabling default features, otherwise the +//! default `mock` stays enabled and silently wins (you link the stub): +//! +//! ```text +//! cargo build --no-default-features --features host-real # or riscv64 +//! ``` +//! +//! # Reproducibility invariant +//! +//! `host-real` and `riscv64` compile libcma with SIMD-free / generic flags +//! (`-DBOOST_UNORDERED_DISABLE_SSE2`, `-DBOOST_UNORDERED_DISABLE_NEON`, +//! `-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION`) so the on-disk 32-byte account +//! records (`balance` u64 little-endian | `owner` 20 bytes | 4 bytes padding) are +//! byte-identical across x86_64 and riscv64. That invariant is what makes +//! off-chain prediction with `host-real` sound: the host reproduces, byte for +//! byte, exactly what the machine computes on-chain. +//! +//! See [`ledger::Ledger`] for the ledger wrapper and its thread-safety contract. + +// --------------------------------------------------------------------------- +// Backend selection guard. +// +// Exactly one backend feature must be enabled — they are mutually exclusive: +// `mock` — in-memory STUB ledger (default; NOT real libcma, never use in production). +// `host-real` — real C++ libcma compiled for the host (x86_64). +// `riscv64` — real C++ libcma cross-compiled for the Cartesi machine (riscv64). +// +// The build.rs link gate keys off `mock`, so enabling two backends (e.g. leaving the +// default `mock` on while adding `host-real`) would silently link the fake ledger. Turn +// that footgun into a hard compile error. +#[cfg(any( + all(feature = "mock", feature = "host-real"), + all(feature = "mock", feature = "riscv64"), + all(feature = "host-real", feature = "riscv64"), +))] +compile_error!( + "libcma_binding_rust: more than one backend feature is enabled, but they are mutually \ + exclusive — enable exactly one of `mock`, `host-real`, or `riscv64`; for a real build use \ + `default-features = false, features = [\"host-real\"]` (or `riscv64`). If you enabled a real \ + backend without `default-features = false`, the default `mock` feature is still on — that is \ + almost certainly the cause; disable default features." +); + +#[cfg(not(any(feature = "mock", feature = "host-real", feature = "riscv64")))] +compile_error!( + "libcma_binding_rust: no backend feature is enabled — enable exactly one of `mock`, \ + `host-real`, or `riscv64`; for a real build use `default-features = false, \ + features = [\"host-real\"]` (or `riscv64`)." +); +// --------------------------------------------------------------------------- + pub mod bindings { #![allow(non_upper_case_globals)] #![allow(non_camel_case_types)] @@ -5,18 +73,27 @@ pub mod bindings { include!(concat!(env!("OUT_DIR"), "/bindings.rs")); } -#[cfg(feature = "native")] +#[cfg(feature = "mock")] mod mocks; pub mod error; -pub mod types; -pub mod ledger; pub mod helpers; +pub mod ledger; pub mod parser; +pub mod types; pub use error::{LedgerError, ParserError}; pub use ledger::{ Ledger, LedgerAsset, LedgerBufferConfig, LedgerFileConfig, LedgerSingleFileConfig, }; -pub use parser::{CmaParserInputType, CmaParserVoucherType, CmaParserError, CmaVoucher, CmaParserUnidentifiedInput, CmaParserEtherDeposit, CmaParserErc20Deposit, CmaParserErc721Deposit, CmaParserErc1155SingleDeposit, CmaParserErc1155BatchDeposit, CmaParserEtherWithdrawal, CmaParserErc20Withdrawal, CmaParserErc721Withdrawal, CmaParserErc1155SingleWithdrawal, CmaParserErc1155BatchWithdrawal, CmaParserEtherTransfer, CmaParserErc20Transfer, CmaParserErc721Transfer, CmaParserErc1155SingleTransfer, CmaParserErc1155BatchTransfer, CmaParserBalance, CmaParserSupply, CmaParserInput, CmaParserInputData, cma_parser_get_last_error_message}; -pub use types::*; \ No newline at end of file +pub use parser::{ + cma_parser_get_last_error_message, CmaParserBalance, CmaParserErc1155BatchDeposit, + CmaParserErc1155BatchTransfer, CmaParserErc1155BatchWithdrawal, CmaParserErc1155SingleDeposit, + CmaParserErc1155SingleTransfer, CmaParserErc1155SingleWithdrawal, CmaParserErc20Deposit, + CmaParserErc20Transfer, CmaParserErc20Withdrawal, CmaParserErc721Deposit, + CmaParserErc721Transfer, CmaParserErc721Withdrawal, CmaParserError, CmaParserEtherDeposit, + CmaParserEtherTransfer, CmaParserEtherWithdrawal, CmaParserInput, CmaParserInputData, + CmaParserInputType, CmaParserSupply, CmaParserUnidentifiedInput, CmaParserVoucherType, + CmaVoucher, +}; +pub use types::*; diff --git a/src/mocks.rs b/src/mocks.rs index c2dc86f..38a756b 100644 --- a/src/mocks.rs +++ b/src/mocks.rs @@ -1,8 +1,24 @@ -#![cfg(feature = "native")] +//! MOCK libcma backend — a behavioural STUB for compile/plumbing tests ONLY. +//! +//! This module provides in-memory, thread-local stand-ins for the `cma_*` FFI symbols so the +//! crate can be built and exercised without the real C++ libcma (and without a C++ toolchain or +//! network). It is enabled by the default `mock` feature. +//! +//! It does NOT reproduce libcma's real ledger / records-image semantics. In particular the +//! `cma_ledger_init_single_*` stubs ignore `asset_type`, `token_address`, and `n_accounts` and +//! simply delegate to `cma_ledger_init`, so the account records they produce are NOT byte-identical +//! to the real single-asset records image. Balances, overflow behaviour, and encoding are likewise +//! only approximated. +//! +//! Consequences: tests run against this mock validate plumbing (pointer/marshalling/error paths), +//! NOT ledger correctness. Passing here is NOT evidence that a change is correct against real +//! libcma. Build with the `host-real` feature (`default-features = false, features = ["host-real"]`) +//! to validate against the actual C++ ledger. +#![cfg(feature = "mock")] use crate::bindings; -use std::collections::HashMap; use std::cell::RefCell; +use std::collections::HashMap; use std::ptr; // Simple in-memory storage for mock ledger @@ -13,7 +29,7 @@ struct MockLedgerState { accounts: HashMap, balances: HashMap<(u64, u64), [u8; 32]>, // (asset_id, account_id) -> balance asset_lookup: HashMap<([u8; 20], [u8; 32]), u64>, // (token_addr, token_id) -> asset_id - account_lookup: HashMap<[u8; 20], u64>, // wallet_address -> account_id + account_lookup: HashMap<[u8; 20], u64>, // wallet_address -> account_id } #[allow(dead_code)] @@ -540,8 +556,7 @@ pub unsafe extern "C" fn cma_parser_decode_advance( out: *mut bindings::cma_parser_input_t, ) -> i32 { if !out.is_null() { - (*out).type_ = - bindings::cma_parser_input_type_t_CMA_PARSER_INPUT_TYPE_ETHER_DEPOSIT; + (*out).type_ = bindings::cma_parser_input_type_t_CMA_PARSER_INPUT_TYPE_ETHER_DEPOSIT; // union access generated by bindgen: let dep = &mut (*out).__bindgen_anon_1.ether_deposit; diff --git a/src/parser.rs b/src/parser.rs index 2352e27..5e72f46 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,12 +1,111 @@ use crate::helpers::hex_to_string; -use ethers_core::abi::{ParamType, Token, decode, encode}; -use ethers_core::types::{Address, Bytes, U256}; -use ethers_core::utils::{id, to_checksum}; +use alloy_primitives::{Address, Bytes, U256}; use hex; use json::JsonValue; use std::cell::RefCell; +use self::abi_compat::{decode, encode, id, to_checksum, ParamType, Token}; + +/// Minimal `ethers_core::abi`-shaped adapter over `alloy-dyn-abi`, covering only the ABI +/// variants this parser uses (`Uint(256)`, `Bytes`, `Address`, `Array`). ethers-rs is EOL; +/// `encode`/`decode` map to alloy's `abi_encode_params`/`abi_decode_params`, the byte-for-byte +/// equivalents of ethers' free `encode`/`decode` — the parser's ABI test vectors pin this. +mod abi_compat { + use alloy_dyn_abi::{DynSolType, DynSolValue}; + use alloy_primitives::{keccak256, Address, U256}; + + #[derive(Clone, Debug)] + pub enum ParamType { + Uint(usize), + Bytes, + Address, + Array(Box), + } + + #[derive(Clone, Debug)] + pub enum Token { + Uint(U256), + Bytes(Vec), + Address(Address), + Array(Vec), + } + + impl ParamType { + fn to_dyn(&self) -> DynSolType { + match self { + ParamType::Uint(bits) => DynSolType::Uint(*bits), + ParamType::Bytes => DynSolType::Bytes, + ParamType::Address => DynSolType::Address, + ParamType::Array(inner) => DynSolType::Array(Box::new(inner.to_dyn())), + } + } + } + + impl Token { + fn to_dyn(&self) -> DynSolValue { + match self { + Token::Uint(v) => DynSolValue::Uint(*v, 256), + Token::Bytes(b) => DynSolValue::Bytes(b.clone()), + Token::Address(a) => DynSolValue::Address(*a), + Token::Array(items) => { + DynSolValue::Array(items.iter().map(Token::to_dyn).collect()) + } + } + } + + fn from_dyn(value: &DynSolValue) -> Option { + match value { + DynSolValue::Uint(v, _) => Some(Token::Uint(*v)), + DynSolValue::Bytes(b) => Some(Token::Bytes(b.clone())), + DynSolValue::Address(a) => Some(Token::Address(*a)), + DynSolValue::Array(items) | DynSolValue::FixedArray(items) => items + .iter() + .map(Token::from_dyn) + .collect::>>() + .map(Token::Array), + _ => None, + } + } + } + + /// ABI-encode tokens as top-level params — equivalent of ethers `encode(&[Token])`. + pub fn encode(tokens: &[Token]) -> Vec { + DynSolValue::Tuple(tokens.iter().map(Token::to_dyn).collect()).abi_encode_params() + } + + /// Opaque decode error; call sites only map it to their own error type. + #[derive(Debug)] + pub struct AbiDecodeError; + + /// ABI-decode top-level params — equivalent of ethers `decode(&[ParamType], data)`. + pub fn decode(types: &[ParamType], data: &[u8]) -> Result, AbiDecodeError> { + let tuple_ty = DynSolType::Tuple(types.iter().map(ParamType::to_dyn).collect()); + match tuple_ty + .abi_decode_params(data) + .map_err(|_| AbiDecodeError)? + { + DynSolValue::Tuple(items) => items + .iter() + .map(Token::from_dyn) + .collect::>>() + .ok_or(AbiDecodeError), + _ => Err(AbiDecodeError), + } + } + + /// 4-byte function selector — equivalent of `ethers_core::utils::id`. + pub fn id(signature: &str) -> [u8; 4] { + let hash = keccak256(signature.as_bytes()); + [hash[0], hash[1], hash[2], hash[3]] + } + + /// Checksummed address string — equivalent of `ethers_core::utils::to_checksum`. + pub fn to_checksum(address: &Address, chain_id: Option) -> String { + address.to_checksum(chain_id) + } +} + thread_local! { static LAST_PARSER_ERROR: RefCell = RefCell::new(String::new()); } @@ -19,8 +118,7 @@ fn payload_bytes(input: &JsonValue) -> Result, CmaParserError> { let payload_hex = input["data"]["payload"] .as_str() .ok_or(CmaParserError::MalformedInput)?; - hex::decode(payload_hex.trim_start_matches("0x")) - .map_err(|_| CmaParserError::MalformedInput) + hex::decode(payload_hex.trim_start_matches("0x")).map_err(|_| CmaParserError::MalformedInput) } fn require_len(bytes: &[u8], min: usize) -> Result<(), CmaParserError> { @@ -35,11 +133,8 @@ fn decode_abi_tail_two_bytes(tail: &[u8]) -> Result<(Bytes, Bytes), CmaParserErr if tail.is_empty() { return Ok((Bytes::from(vec![]), Bytes::from(vec![]))); } - let decoded = decode( - &[ParamType::Bytes, ParamType::Bytes], - tail, - ) - .map_err(|_| CmaParserError::MalformedInput)?; + let decoded = decode(&[ParamType::Bytes, ParamType::Bytes], tail) + .map_err(|_| CmaParserError::MalformedInput)?; let base = match &decoded[0] { Token::Bytes(b) => Bytes::from(b.clone()), _ => return Err(CmaParserError::MalformedInput), @@ -128,7 +223,7 @@ fn parse_hex_account_id(value: &str) -> Result { return Err(CmaParserError::MalformedInput); } bytes[offset..offset + decoded.len()].copy_from_slice(&decoded); - Ok(U256::from_big_endian(&bytes)) + Ok(U256::from_be_slice(&bytes)) } fn parse_hex_token_id(value: &str) -> Result { @@ -150,7 +245,7 @@ fn parse_hex_token_id(value: &str) -> Result { return Err(CmaParserError::MalformedInput); } bytes[offset..offset + decoded.len()].copy_from_slice(&decoded); - Ok(U256::from_big_endian(&bytes)) + Ok(U256::from_be_slice(&bytes)) } fn parse_token_address(value: &str) -> Result { @@ -196,7 +291,7 @@ pub enum TxHexCodes { TransferErc1155Single = 0xe1c913ed, // Bytecode for solidity TransferErc1155Batch(address,bytes32,uint256[],uint256[],bytes) = 638ac6f9 TransferErc1155Batch = 0x638ac6f9, - Unidentified + Unidentified, } impl TxHexCodes { @@ -212,7 +307,7 @@ impl TxHexCodes { Self::TransferErc721 => "0xaf615a5a", Self::TransferErc1155Single => "0xe1c913ed", Self::TransferErc1155Batch => "0x638ac6f9", - Self::Unidentified => "0x00000000" + Self::Unidentified => "0x00000000", } } @@ -237,13 +332,21 @@ impl TxHexCodes { Self::WithdrawEther => CmaParserInputType::CmaParserInputTypeEtherWithdrawal, Self::WithdrawErc20 => CmaParserInputType::CmaParserInputTypeErc20Withdrawal, Self::WithdrawErc721 => CmaParserInputType::CmaParserInputTypeErc721Withdrawal, - Self::WithdrawErc1155Single => CmaParserInputType::CmaParserInputTypeErc1155SingleWithdrawal, - Self::WithdrawErc1155Batch => CmaParserInputType::CmaParserInputTypeErc1155BatchWithdrawal, + Self::WithdrawErc1155Single => { + CmaParserInputType::CmaParserInputTypeErc1155SingleWithdrawal + } + Self::WithdrawErc1155Batch => { + CmaParserInputType::CmaParserInputTypeErc1155BatchWithdrawal + } Self::TransferEther => CmaParserInputType::CmaParserInputTypeEtherTransfer, Self::TransferErc20 => CmaParserInputType::CmaParserInputTypeErc20Transfer, Self::TransferErc721 => CmaParserInputType::CmaParserInputTypeErc721Transfer, - Self::TransferErc1155Single => CmaParserInputType::CmaParserInputTypeErc1155SingleTransfer, - Self::TransferErc1155Batch => CmaParserInputType::CmaParserInputTypeErc1155BatchTransfer, + Self::TransferErc1155Single => { + CmaParserInputType::CmaParserInputTypeErc1155SingleTransfer + } + Self::TransferErc1155Batch => { + CmaParserInputType::CmaParserInputTypeErc1155BatchTransfer + } Self::Unidentified => CmaParserInputType::CmaParserInputTypeUnidentified, } } @@ -304,7 +407,9 @@ impl CmaParserInputType { "Erc721Transfer" => CmaParserInputType::CmaParserInputTypeErc721Transfer, "Erc1155SingleTransfer" => CmaParserInputType::CmaParserInputTypeErc1155SingleTransfer, "Erc1155BatchTransfer" => CmaParserInputType::CmaParserInputTypeErc1155BatchTransfer, - "ledger_getBalance" | "ledgerGetBalance" => CmaParserInputType::CmaParserInputTypeBalance, + "ledger_getBalance" | "ledgerGetBalance" => { + CmaParserInputType::CmaParserInputTypeBalance + } "ledger_getTotalSupply" | "ledgerGetTotalSupply" => { CmaParserInputType::CmaParserInputTypeSupply } @@ -614,10 +719,12 @@ fn handle_unidentified_method(input: JsonValue) -> Result Result { @@ -625,7 +732,7 @@ fn handle_parse_ether_deposit(input: JsonValue) -> Result Result Result Result Result { @@ -679,8 +785,8 @@ fn handle_parse_erc1155_single_deposit( let token = Address::from_slice(&bytes[0..20]); let sender = Address::from_slice(&bytes[20..40]); - let token_id = U256::from_big_endian(&bytes[40..72]); - let amount = U256::from_big_endian(&bytes[72..104]); + let token_id = U256::from_be_slice(&bytes[40..72]); + let amount = U256::from_be_slice(&bytes[72..104]); let (base_layer_data, exec_layer_data) = decode_abi_tail_two_bytes(&bytes[104..])?; Ok(CmaParserInputData::Erc1155SingleDeposit( @@ -742,11 +848,8 @@ fn handle_ether_withdrawal(input: JsonValue) -> Result *v, @@ -757,11 +860,13 @@ fn handle_ether_withdrawal(input: JsonValue) -> Result return Err(CmaParserError::MalformedInput), }; - Ok(CmaParserInputData::EtherWithdrawal(CmaParserEtherWithdrawal { - receiver, - amount, - exec_layer_data: exec_layer_hex(exec_layer_byte), - })) + Ok(CmaParserInputData::EtherWithdrawal( + CmaParserEtherWithdrawal { + receiver, + amount, + exec_layer_data: exec_layer_hex(exec_layer_byte), + }, + )) } fn handle_erc20_withdrawal(input: JsonValue) -> Result { @@ -788,12 +893,14 @@ fn handle_erc20_withdrawal(input: JsonValue) -> Result return Err(CmaParserError::MalformedInput), }; - Ok(CmaParserInputData::Erc20Withdrawal(CmaParserErc20Withdrawal { - receiver, - token, - amount, - exec_layer_data: exec_layer_hex(exec_layer_byte), - })) + Ok(CmaParserInputData::Erc20Withdrawal( + CmaParserErc20Withdrawal { + receiver, + token, + amount, + exec_layer_data: exec_layer_hex(exec_layer_byte), + }, + )) } fn handle_erc721_withdrawal(input: JsonValue) -> Result { @@ -820,12 +927,14 @@ fn handle_erc721_withdrawal(input: JsonValue) -> Result return Err(CmaParserError::MalformedInput), }; - Ok(CmaParserInputData::Erc721Withdrawal(CmaParserErc721Withdrawal { - receiver, - token, - token_id, - exec_layer_data: exec_layer_hex(exec_layer_byte), - })) + Ok(CmaParserInputData::Erc721Withdrawal( + CmaParserErc721Withdrawal { + receiver, + token, + token_id, + exec_layer_data: exec_layer_hex(exec_layer_byte), + }, + )) } fn handle_erc1155_single_withdrawal( @@ -874,9 +983,7 @@ fn handle_erc1155_single_withdrawal( )) } -fn handle_erc1155_batch_withdrawal( - input: JsonValue, -) -> Result { +fn handle_erc1155_batch_withdrawal(input: JsonValue) -> Result { let bytes = payload_bytes(&input)?; let encoded_args = decode_after_selector(&bytes)?; let receiver = withdrawal_receiver(&input)?; @@ -1027,17 +1134,17 @@ fn handle_erc721_transfer(input: JsonValue) -> Result return Err(CmaParserError::MalformedInput), }; - Ok(CmaParserInputData::Erc721Transfer(CmaParserErc721Transfer { - receiver, - token, - token_id, - exec_layer_data: exec_layer_hex(exec_layer_byte), - })) + Ok(CmaParserInputData::Erc721Transfer( + CmaParserErc721Transfer { + receiver, + token, + token_id, + exec_layer_data: exec_layer_hex(exec_layer_byte), + }, + )) } -fn handle_erc1155_single_transfer( - input: JsonValue, -) -> Result { +fn handle_erc1155_single_transfer(input: JsonValue) -> Result { let bytes = payload_bytes(&input)?; let encoded_args = decode_after_selector(&bytes)?; @@ -1085,9 +1192,7 @@ fn handle_erc1155_single_transfer( )) } -fn handle_erc1155_batch_transfer( - input: JsonValue, -) -> Result { +fn handle_erc1155_batch_transfer(input: JsonValue) -> Result { let bytes = payload_bytes(&input)?; let encoded_args = decode_after_selector(&bytes)?; @@ -1163,7 +1268,7 @@ fn cma_decode_advance_inner( input: data, }); } - CmaParserInputType::CmaParserInputTypeErc1155SingleDeposit=> { + CmaParserInputType::CmaParserInputTypeErc1155SingleDeposit => { return handle_parse_erc1155_single_deposit(input).map(|data| CmaParserInput { req_type, input: data, @@ -1210,12 +1315,8 @@ fn cma_decode_advance_inner( CmaParserInputType::CmaParserInputTypeErc1155BatchWithdrawal => { handle_erc1155_batch_withdrawal(input) } - CmaParserInputType::CmaParserInputTypeEtherTransfer => { - handle_ether_transfer(input) - } - CmaParserInputType::CmaParserInputTypeErc20Transfer => { - handle_erc20_transfer(input) - } + CmaParserInputType::CmaParserInputTypeEtherTransfer => handle_ether_transfer(input), + CmaParserInputType::CmaParserInputTypeErc20Transfer => handle_erc20_transfer(input), CmaParserInputType::CmaParserInputTypeErc721Transfer => { handle_erc721_transfer(input) } @@ -1231,7 +1332,10 @@ fn cma_decode_advance_inner( _ => Err(CmaParserError::IncompatibleInput), }; - result.map(|data| CmaParserInput { req_type, input: data }) + result.map(|data| CmaParserInput { + req_type, + input: data, + }) } _ => Err(CmaParserError::Unknown), } @@ -1262,8 +1366,8 @@ fn handle_ledger_get_balance(parsed_json: JsonValue) -> Result Result Result Result { - handle_ledger_get_balance(payload_json).map(|data| CmaParserInput { + CmaParserInputType::CmaParserInputTypeBalance => handle_ledger_get_balance(payload_json) + .map(|data| CmaParserInput { req_type, input: CmaParserInputData::Balance(data), - }) - } + }), CmaParserInputType::CmaParserInputTypeSupply => { handle_ledger_get_supply(payload_json).map(|data| CmaParserInput { req_type, @@ -1356,8 +1457,7 @@ fn handle_ether_voucher_encoding( voucher_request: &CmaVoucherFieldType, ) -> Result { if let CmaVoucherFieldType::EtherVoucherFields(fields) = voucher_request { - let mut value_bytes = [0u8; 32]; - fields.amount.to_big_endian(&mut value_bytes); + let value_bytes = fields.amount.to_be_bytes::<32>(); Ok(CmaVoucher { destination: to_checksum(&fields.receiver, None), @@ -1450,11 +1550,8 @@ fn handle_erc1155_batch_voucher_encoding( return Err(CmaParserError::MalformedInput); } - let token_id_tokens: Vec = fields - .token_ids - .iter() - .map(|id| Token::Uint(*id)) - .collect(); + let token_id_tokens: Vec = + fields.token_ids.iter().map(|id| Token::Uint(*id)).collect(); let amount_tokens: Vec = fields .amounts .iter() diff --git a/src/types.rs b/src/types.rs index 3fa995a..3920870 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,6 @@ use crate::bindings; // use std::fmt; -pub use ethers_core::types::{Address, U256}; +pub use alloy_primitives::{Address, U256}; use std::str::FromStr; pub const ADDRESS_LENGTH: usize = 20; @@ -41,15 +41,17 @@ impl AddressCBindingsExt for Address { } fn as_array(&self) -> [u8; ADDRESS_LENGTH] { - self.0 + self.0 .0 } fn as_bytes(&self) -> &[u8; ADDRESS_LENGTH] { - &self.0 + &self.0 .0 } fn to_c(&self) -> bindings::cmt_abi_address_t { - bindings::cmt_abi_address_t { data: self.as_array() } + bindings::cmt_abi_address_t { + data: self.as_array(), + } } fn from_c(c_addr: &bindings::cmt_abi_address_t) -> Self { @@ -81,11 +83,11 @@ pub trait U256CBindingsExt { impl U256CBindingsExt for U256 { fn new(bytes: [u8; U256_LENGTH]) -> Self { - U256::from_big_endian(&bytes) + U256::from_be_slice(&bytes) } fn zero() -> Self { - U256::zero() + U256::ZERO } fn from_u64(value: u64) -> Self { @@ -93,36 +95,37 @@ impl U256CBindingsExt for U256 { } fn from_be_bytes(bytes: [u8; U256_LENGTH]) -> Self { - U256::from_big_endian(&bytes) + U256::from_be_slice(&bytes) } fn from_slice(slice: &[u8]) -> Result { if slice.len() != U256_LENGTH { return Err(format!("U256 must be 32 bytes, got {}", slice.len())); } - Ok(U256::from_big_endian(slice)) + Ok(U256::from_be_slice(slice)) } fn as_be_bytes(&self) -> [u8; U256_LENGTH] { - let mut out = [0u8; U256_LENGTH]; - self.to_big_endian(&mut out); - out + self.to_be_bytes::() } fn to_u128_opt(&self) -> Option { - if self.bits() > 128 { + // `bit_len` counts significant bits; > 128 means it won't fit a u128. + if self.bit_len() > 128 { None } else { - Some(self.low_u128()) + Some(self.to::()) } } fn to_c(&self) -> bindings::cmt_abi_u256_t { - bindings::cmt_abi_u256_t { data: self.as_be_bytes() } + bindings::cmt_abi_u256_t { + data: self.as_be_bytes(), + } } fn from_c(c_u256: &bindings::cmt_abi_u256_t) -> Self { - U256::from_big_endian(&c_u256.data) + U256::from_be_slice(&c_u256.data) } } @@ -227,9 +230,7 @@ pub struct OwnedCBytes { impl OwnedCBytes { pub fn new(data: impl Into>) -> Self { - Self { - inner: data.into(), - } + Self { inner: data.into() } } pub fn as_c_bytes(&self) -> bindings::cmt_abi_bytes_t { diff --git a/tests/host_real_records_layout.rs b/tests/host_real_records_layout.rs new file mode 100644 index 0000000..0a70aa4 --- /dev/null +++ b/tests/host_real_records_layout.rs @@ -0,0 +1,132 @@ +//! host-real records byte-layout + reproducibility test. +//! +//! Builds/runs ONLY under `--no-default-features --features host-real` (the real C++ libcma +//! compiled for the host). Under the default `mock` backend the caller buffer is ignored, so +//! there are no records to inspect and the whole file compiles to nothing. +//! +//! ```sh +//! cargo test --no-default-features --features host-real --test host_real_records_layout -- --nocapture +//! ``` +//! +//! Why this test exists: host↔machine reproducibility (the sequencer predicting the Cartesi +//! machine's ledger, the watchdog byte-comparing images, the emergency-withdrawal proof) all +//! depend on the on-drive account record having a FIXED, stable byte layout. This test locks +//! that layout down and asserts it is deterministic across runs. The layout is defined by +//! `third_party/machine-asset-tools/src/ledger_impl.h`: +//! +//! ```c +//! struct alignas(32) cma_ledger_single_balance { +//! uint64_t balance; // bytes 0..8 little-endian (interprocess.hpp asserts LE) +//! cma_abi_address_t address; // bytes 8..28 20-byte owner wallet address +//! uint8_t padding[4]; // bytes 28..32 zero pad to a 32-byte power-of-two record +//! }; +//! static_assert(sizeof(cma_ledger_single_balance) == 32); +//! ``` +#![cfg(feature = "host-real")] + +use libcma_binding_rust::ledger::{Ledger, LedgerAsset}; +use libcma_binding_rust::{Address, U256}; + +const MEM_LEN: usize = 4 * 1024 * 1024; +const MAX_ACCOUNTS: usize = 4096; +const RECORDS_PREFIX: usize = MAX_ACCOUNTS * 32; // 128 KiB proven records region + +// Record byte layout (see module docs / ledger_impl.h). These offsets are the invariant. +const RECORD_SIZE: usize = 32; +const BALANCE_RANGE: std::ops::Range = 0..8; // u64 little-endian +const OWNER_RANGE: std::ops::Range = 8..28; // 20-byte owner address +const PAD_RANGE: std::ops::Range = 28..32; // 4-byte zero pad + +fn token() -> Address { + "0x88A2120B7068E78692C8fd12E751d610B6377E4d" + .parse() + .unwrap() +} +fn alice() -> Address { + "0x1111111111111111111111111111111111111111" + .parse() + .unwrap() +} +fn bob() -> Address { + "0x2222222222222222222222222222222222222222" + .parse() + .unwrap() +} + +/// Initialise a single-asset (ERC-20) buffer-backed ledger over `buf` and credit each +/// `(owner, balance)`. The ledger state lives in `buf`'s 32-byte records prefix once this +/// returns. Mirrors the setup in `tests/host_real_smoke.rs`. +fn build_ledger(buf: &mut [u8], credits: &[(Address, u64)]) { + let mut ledger = Ledger::new().expect("ledger init"); + ledger + .init_single_from_buffer(buf, MAX_ACCOUNTS, LedgerAsset::Erc20(token())) + .expect("init single buffer"); + let asset = ledger + .retrieve_erc20_asset_via_address(token()) + .expect("asset"); + for &(owner, bal) in credits { + let account = ledger.retrieve_account_via_address(owner).expect("account"); + ledger + .deposit(asset, account, U256::from(bal)) + .expect("deposit"); + } +} + +/// Return the 32-byte record in `buf`'s records prefix whose owner field (bytes 8..28) +/// equals `owner`, or `None` if no such record exists. +fn find_record(buf: &[u8], owner: Address) -> Option<&[u8]> { + let want = owner.0 .0; + buf[..RECORDS_PREFIX] + .chunks_exact(RECORD_SIZE) + .find(|rec| rec[OWNER_RANGE] == want) +} + +/// Each credited account's 32-byte record is exactly `balance(u64 LE) | owner(20) | pad(4)`. +#[test] +fn record_layout_is_balance_owner_pad() { + let credits = [(alice(), 250u64), (bob(), 70u64)]; + + let mut buf = vec![0u8; MEM_LEN]; + build_ledger(&mut buf, &credits); + + for &(owner, bal) in &credits { + let rec = find_record(&buf, owner).unwrap_or_else(|| { + panic!("no record for {owner:?} — ledger state is not in the caller buffer") + }); + eprintln!("record for {owner:?} = {}", hex::encode(rec)); + + // balance: u64, little-endian, bytes 0..8 + let got = u64::from_le_bytes(rec[BALANCE_RANGE].try_into().unwrap()); + assert_eq!(got, bal, "balance field (u64 LE) mismatch for {owner:?}"); + + // owner: 20-byte address, bytes 8..28 + assert_eq!( + rec[OWNER_RANGE], owner.0 .0, + "owner field (bytes 8..28) mismatch for {owner:?}" + ); + + // pad: 4 zero bytes, bytes 28..32 + assert_eq!( + rec[PAD_RANGE], [0u8; 4], + "trailing pad (bytes 28..32) must be zero for {owner:?}" + ); + } +} + +/// Identical inputs must produce byte-identical records: the reproducibility property the +/// watchdog byte-compare and the emergency-withdrawal proof depend on. +#[test] +fn records_are_deterministic_across_runs() { + let credits = [(alice(), 250u64), (bob(), 70u64)]; + + let mut buf_a = vec![0u8; MEM_LEN]; + let mut buf_b = vec![0u8; MEM_LEN]; + build_ledger(&mut buf_a, &credits); + build_ledger(&mut buf_b, &credits); + + assert_eq!( + buf_a[..RECORDS_PREFIX], + buf_b[..RECORDS_PREFIX], + "identical credits must yield byte-identical records (host reproducibility)" + ); +} diff --git a/tests/host_real_smoke.rs b/tests/host_real_smoke.rs index 11b5d40..68b01e5 100644 --- a/tests/host_real_smoke.rs +++ b/tests/host_real_smoke.rs @@ -25,13 +25,19 @@ const MAX_ACCOUNTS: usize = 4096; const RECORDS_PREFIX: usize = MAX_ACCOUNTS * 32; // 128 KiB proven region fn token() -> Address { - "0x88A2120B7068E78692C8fd12E751d610B6377E4d".parse().unwrap() + "0x88A2120B7068E78692C8fd12E751d610B6377E4d" + .parse() + .unwrap() } fn alice() -> Address { - "0x1111111111111111111111111111111111111111".parse().unwrap() + "0x1111111111111111111111111111111111111111" + .parse() + .unwrap() } fn bob() -> Address { - "0x2222222222222222222222222222222222222222".parse().unwrap() + "0x2222222222222222222222222222222222222222" + .parse() + .unwrap() } /// Real libcma links and computes balances on the host (i.e. we are NOT on the mock). @@ -43,9 +49,15 @@ fn real_libcma_links_and_computes_balances_on_host() { .init_single_from_buffer(&mut buf, MAX_ACCOUNTS, LedgerAsset::Erc20(token())) .expect("init single buffer"); - let asset = ledger.retrieve_erc20_asset_via_address(token()).expect("asset"); - let account = ledger.retrieve_account_via_address(alice()).expect("account"); - ledger.deposit(asset, account, U256::from(100)).expect("deposit"); + let asset = ledger + .retrieve_erc20_asset_via_address(token()) + .expect("asset"); + let account = ledger + .retrieve_account_via_address(alice()) + .expect("account"); + ledger + .deposit(asset, account, U256::from(100)) + .expect("deposit"); assert_eq!( ledger.get_balance(asset, account).expect("balance"), @@ -63,12 +75,18 @@ fn records_prefix_holds_owner_and_balance() { ledger .init_single_from_buffer(&mut buf, MAX_ACCOUNTS, LedgerAsset::Erc20(token())) .expect("init single buffer"); - let asset = ledger.retrieve_erc20_asset_via_address(token()).expect("asset"); - let acc = ledger.retrieve_account_via_address(alice()).expect("account"); - ledger.deposit(asset, acc, U256::from(250)).expect("deposit"); + let asset = ledger + .retrieve_erc20_asset_via_address(token()) + .expect("asset"); + let acc = ledger + .retrieve_account_via_address(alice()) + .expect("account"); + ledger + .deposit(asset, acc, U256::from(250)) + .expect("deposit"); // Scan the 128 KiB records prefix (32-byte strides) for alice's address bytes. - let addr = alice().to_fixed_bytes(); + let addr = alice().0 .0; let mut found = None; for (i, rec) in buf[..RECORDS_PREFIX].chunks_exact(32).enumerate() { if rec.windows(20).any(|w| w == addr) { @@ -98,7 +116,8 @@ fn restore_by_recredit_round_trips() { let a1 = l1.retrieve_erc20_asset_via_address(token).expect("asset"); let alice_id = l1.retrieve_account_via_address(alice()).expect("alice"); let bob_id = l1.retrieve_account_via_address(bob()).expect("bob"); - l1.deposit(a1, alice_id, U256::from(250)).expect("dep alice"); + l1.deposit(a1, alice_id, U256::from(250)) + .expect("dep alice"); l1.deposit(a1, bob_id, U256::from(70)).expect("dep bob"); let supply1 = l1.get_total_supply(a1).expect("supply1"); @@ -119,7 +138,10 @@ fn restore_by_recredit_round_trips() { // Balances and total supply match the original. let alice2 = l2.retrieve_account_via_address(alice()).expect("alice2"); let bob2 = l2.retrieve_account_via_address(bob()).expect("bob2"); - assert_eq!(l2.get_balance(a2, alice2).expect("bal alice2"), U256::from(250)); + assert_eq!( + l2.get_balance(a2, alice2).expect("bal alice2"), + U256::from(250) + ); assert_eq!(l2.get_balance(a2, bob2).expect("bal bob2"), U256::from(70)); assert_eq!(l2.get_total_supply(a2).expect("supply2"), supply1); diff --git a/tests/ledger_tests.rs b/tests/ledger_tests.rs index 2570b5e..4d9f0e1 100644 --- a/tests/ledger_tests.rs +++ b/tests/ledger_tests.rs @@ -1,3 +1,33 @@ +//! Behavioural tests for the libcma `Ledger`. +//! +//! ## Asset types — fungible vs. non-fungible +//! +//! libcma has two token-backed asset types, and the distinction is load-bearing: +//! +//! * [`AssetType::TokenAddress`] — a **fungible** token (ERC-20), keyed by token address +//! only. Its total supply is a full 256-bit integer, so any balance is valid. +//! * [`AssetType::TokenAddressId`] — a **non-fungible** token (ERC-721 / a single ERC-1155 +//! id), keyed by *(token address, token id)*. Such an asset is unique: real libcma +//! enforces that its supply can only ever go `0 -> 1` and that the only legal deposit is +//! exactly `1` (`src/ledger_impl.cpp`). Depositing e.g. `1000` returns `SupplyOverflow` — +//! that error is the "you violated NFT uniqueness" signal, **not** an arithmetic overflow +//! (the supply field is 256-bit and nowhere near full). +//! +//! These fungible tests therefore use `TokenAddress`. The mock backend does not enforce the +//! NFT rule, so an earlier version of these tests used `TokenAddressId` with fungible amounts +//! and only passed against the mock; they failed against real libcma. See +//! `test_nft_asset_deposit_is_capped_at_one` for the enforced NFT behaviour. +//! +//! ## Backend-agnostic assertions +//! +//! These run under BOTH the default `mock` backend and the real libcma backends +//! (`host-real` / `riscv64`), which differ in incidental ways — most notably the mock uses +//! 1-based asset/account ids (reserving id 1 for the Base asset) while real libcma is 0-based. +//! So the tests assert *relationships* (a freshly created id is found again unchanged) rather +//! than absolute id values, and probe "not found" via the `Find` retrieve operation (which +//! errors on both) rather than `get_balance` (which returns 0 for an unknown pair on real +//! libcma but errors on the mock). + use libcma_binding_rust::{Ledger, LedgerError, *}; use std::fs::OpenOptions; use std::time::{SystemTime, UNIX_EPOCH}; @@ -161,20 +191,33 @@ fn test_create_asset_by_token_address() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); let token_addr = test_token_address(); - let token_id = U256::from_u64(1); - // Create an asset using token address - let asset_id = ledger.retrieve_asset( - None, // No existing asset_id - Some(token_addr), - Some(token_id), - AssetType::TokenAddressId, - RetrieveOperation::Create, - ); + // Create a fungible (ERC-20) asset keyed by token address. + let created = ledger + .retrieve_asset( + None, // No existing asset_id + Some(token_addr), + None, + AssetType::TokenAddress, + RetrieveOperation::Create, + ) + .expect("Asset creation should succeed"); - assert!(asset_id.is_ok(), "Asset creation should succeed"); - let asset_id = asset_id.unwrap(); - assert!(asset_id.0 > 0, "Asset ID should be non-zero"); + // Backend-agnostic: the created asset must be findable again under the SAME id (the raw id + // value differs between the mock (1-based) and real libcma (0-based), so don't assert `> 0`). + let found = ledger + .retrieve_asset( + None, + Some(token_addr), + None, + AssetType::TokenAddress, + RetrieveOperation::Find, + ) + .expect("Created asset should be findable"); + assert_eq!( + created, found, + "Find must return the id that Create assigned" + ); } #[test] @@ -216,16 +259,29 @@ fn test_create_account_by_wallet_address() { let wallet_addr = test_account_address(); // Create an account using wallet address - let account_id = ledger.retrieve_account( - None, // No existing account_id - AccountType::WalletAddress, - RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), - ); + let created = ledger + .retrieve_account( + None, // No existing account_id + AccountType::WalletAddress, + RetrieveOperation::Create, + Some(wallet_addr.as_slice()), + ) + .expect("Account creation should succeed"); - assert!(account_id.is_ok(), "Account creation should succeed"); - let account_id = account_id.unwrap(); - assert!(account_id.0 > 0, "Account ID should be non-zero"); + // Backend-agnostic: the account must be findable again under the SAME id (real libcma is + // 0-based, the mock 1-based — so assert the relationship, not a specific value). + let found = ledger + .retrieve_account( + None, + AccountType::WalletAddress, + RetrieveOperation::Find, + Some(wallet_addr.as_slice()), + ) + .expect("Created account should be findable"); + assert_eq!( + created, found, + "Find must return the id that Create assigned" + ); } #[test] @@ -264,15 +320,14 @@ fn test_find_or_create_account() { fn test_deposit_and_balance() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create an asset + // Create a fungible asset let token_addr = test_token_address(); - let token_id = U256::from_u64(100); let asset_id = ledger .retrieve_asset( None, Some(token_addr), - Some(token_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset"); @@ -284,7 +339,7 @@ fn test_deposit_and_balance() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), + Some(wallet_addr.as_slice()), ) .expect("Should create account"); @@ -326,15 +381,14 @@ fn test_deposit_and_balance() { fn test_withdraw() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create asset and account + // Create fungible asset and account let token_addr = test_token_address(); - let token_id = U256::from_u64(200); let asset_id = ledger .retrieve_asset( None, Some(token_addr), - Some(token_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset"); @@ -345,7 +399,7 @@ fn test_withdraw() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), + Some(wallet_addr.as_slice()), ) .expect("Should create account"); @@ -375,36 +429,51 @@ fn test_withdraw() { fn test_insufficient_funds_error() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create asset and account + // Fungible asset. let token_addr = test_token_address(); - let token_id = U256::from_u64(300); let asset_id = ledger .retrieve_asset( None, Some(token_addr), - Some(token_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset"); + // Fund a FIRST account so the asset's total supply is non-zero. libcma checks the asset + // supply for underflow BEFORE the per-account balance, so an empty account only reports + // InsufficientFunds (rather than a supply underflow) once the supply itself can cover it. + let mut funder_bytes = [0u8; 20]; + funder_bytes[0] = 0xF0; + let funder = Address::new(funder_bytes); + let funder_id = ledger + .retrieve_account( + None, + AccountType::WalletAddress, + RetrieveOperation::Create, + Some(funder.as_slice()), + ) + .expect("Should create funder account"); + ledger + .deposit(asset_id, funder_id, U256::from_u64(1000)) + .expect("funder deposit should succeed"); + + // A SECOND, empty account tries to withdraw more than its (zero) balance. let wallet_addr = test_account_address(); let account_id = ledger .retrieve_account( None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), + Some(wallet_addr.as_slice()), ) .expect("Should create account"); - // Try to withdraw without depositing first - let withdraw_amount = U256::from_u64(100); - let result = ledger.withdraw(asset_id, account_id, withdraw_amount); - + let result = ledger.withdraw(asset_id, account_id, U256::from_u64(100)); assert!( result.is_err(), - "Withdraw should fail with insufficient funds" + "Withdraw from an empty account should fail" ); match result.unwrap_err() { LedgerError::InsufficientFunds => { @@ -418,15 +487,14 @@ fn test_insufficient_funds_error() { fn test_transfer() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create asset + // Create a fungible asset let token_addr = test_token_address(); - let token_id = U256::from_u64(400); let asset_id = ledger .retrieve_asset( None, Some(token_addr), - Some(token_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset"); @@ -440,7 +508,7 @@ fn test_transfer() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet1.as_bytes()), + Some(wallet1.as_slice()), ) .expect("Should create account 1"); @@ -452,7 +520,7 @@ fn test_transfer() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet2.as_bytes()), + Some(wallet2.as_slice()), ) .expect("Should create account 2"); @@ -499,17 +567,16 @@ fn test_transfer() { fn test_multiple_assets_and_accounts() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create two different assets + // Create two different fungible assets, keyed by two distinct token addresses. let mut token1_bytes = [0u8; 20]; token1_bytes[0] = 0xA1; let token1_addr = TokenAddress::new(token1_bytes); - let token1_id = U256::from_u64(1); let asset1 = ledger .retrieve_asset( None, Some(token1_addr), - Some(token1_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset 1"); @@ -517,13 +584,12 @@ fn test_multiple_assets_and_accounts() { let mut token2_bytes = [0u8; 20]; token2_bytes[0] = 0xB2; let token2_addr = TokenAddress::new(token2_bytes); - let token2_id = U256::from_u64(2); let asset2 = ledger .retrieve_asset( None, Some(token2_addr), - Some(token2_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset 2"); @@ -535,7 +601,7 @@ fn test_multiple_assets_and_accounts() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), + Some(wallet_addr.as_slice()), ) .expect("Should create account"); @@ -577,22 +643,23 @@ fn test_multiple_assets_and_accounts() { fn test_account_not_found_error() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create asset + // Create a fungible asset. let token_addr = test_token_address(); - let token_id = U256::from_u64(500); let asset_id = ledger .retrieve_asset( None, Some(token_addr), - Some(token_id), - AssetType::TokenAddressId, + None, + AssetType::TokenAddress, RetrieveOperation::Create, ) .expect("Should create asset"); - // Try to get balance for non-existent account + // An OPERATION against a non-existent account must fail with AccountNotFound. (`get_balance` + // is NOT used here: real libcma returns 0 for an unknown (asset, account) pair rather than + // erroring — it is the mutating paths that validate account existence.) let fake_account_id = LedgerAccountId(99999); - let result = ledger.get_balance(asset_id, fake_account_id); + let result = ledger.deposit(asset_id, fake_account_id, U256::from_u64(1)); assert!(result.is_err(), "Should fail for non-existent account"); match result.unwrap_err() { @@ -614,13 +681,14 @@ fn test_asset_not_found_error() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), + Some(wallet_addr.as_slice()), ) .expect("Should create account"); - // Try to get balance for non-existent asset + // An OPERATION against a non-existent asset must fail with AssetNotFound (again via a + // mutating path, not `get_balance`, which returns 0 for unknown pairs on real libcma). let fake_asset_id = LedgerAssetId(99999); - let result = ledger.get_balance(fake_asset_id, account_id); + let result = ledger.deposit(fake_asset_id, account_id, U256::from_u64(1)); assert!(result.is_err(), "Should fail for non-existent asset"); match result.unwrap_err() { @@ -681,20 +749,36 @@ fn test_find_nonexistent_account() { #[test] fn test_retrieve_ether_asset() { + // The Base (ether) asset type is only supported by the buffer/file-backed multi-asset ledger + // (`cma_ledger_memory`). The transient `Ledger::new()` backend (`cma_ledger_basic`) has no + // Base case and returns EINVAL, so back the ledger with a buffer before touching ether. let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - let asset_id = ledger + let mut buffer = vec![0u8; 1024 * 1024]; + ledger + .init_from_buffer(&mut buffer, LedgerBufferConfig::default()) + .expect("Buffer-backed initialization should succeed"); + + let created = ledger .retrieve_ether_assets() .expect("Should create base ether asset"); - assert!(asset_id.0 > 0, "Ether asset ID should be non-zero"); + // Backend-agnostic: retrieving it again (find-or-create) must return the same id. + let again = ledger + .retrieve_ether_assets() + .expect("Should find the existing base ether asset"); + assert_eq!(created, again, "Base ether asset id must be stable"); } +/// The NON-FUNGIBLE (`TokenAddressId`) asset type is capped at supply 1: a single deposit of +/// exactly 1 succeeds, and anything else (a second unit, or an initial amount > 1) is rejected +/// with `SupplyOverflow`. This is real libcma behaviour the mock does not model, so it only runs +/// against a real backend. +#[cfg(any(feature = "host-real", feature = "riscv64"))] #[test] -fn test_large_amounts() { +fn test_nft_asset_deposit_is_capped_at_one() { let mut ledger = Ledger::new().expect("Failed to initialize ledger"); - // Create asset and account let token_addr = test_token_address(); - let token_id = U256::from_u64(700); + let token_id = U256::from_u64(42); let asset_id = ledger .retrieve_asset( None, @@ -703,6 +787,54 @@ fn test_large_amounts() { AssetType::TokenAddressId, RetrieveOperation::Create, ) + .expect("Should create NFT asset"); + + let holder = ledger + .retrieve_account( + None, + AccountType::WalletAddress, + RetrieveOperation::Create, + Some(test_account_address().as_slice()), + ) + .expect("Should create holder account"); + + // Depositing more than one unit of a unique token is rejected. + match ledger.deposit(asset_id, holder, U256::from_u64(5)) { + Err(LedgerError::SupplyOverflow) => {} + other => panic!("NFT deposit > 1 should be SupplyOverflow, got {:?}", other), + } + + // Minting the single unit succeeds... + ledger + .deposit(asset_id, holder, U256::from_u64(1)) + .expect("Minting the single NFT unit should succeed"); + assert_eq!( + ledger.get_total_supply(asset_id).expect("supply"), + U256::from_u64(1), + "NFT supply must be exactly 1" + ); + + // ...but a second unit pushes supply past 1 and is rejected. + match ledger.deposit(asset_id, holder, U256::from_u64(1)) { + Err(LedgerError::SupplyOverflow) => {} + other => panic!("second NFT unit should be SupplyOverflow, got {:?}", other), + } +} + +#[test] +fn test_large_amounts() { + let mut ledger = Ledger::new().expect("Failed to initialize ledger"); + + // Create a fungible asset and account + let token_addr = test_token_address(); + let asset_id = ledger + .retrieve_asset( + None, + Some(token_addr), + None, + AssetType::TokenAddress, + RetrieveOperation::Create, + ) .expect("Should create asset"); let wallet_addr = test_account_address(); @@ -711,7 +843,7 @@ fn test_large_amounts() { None, AccountType::WalletAddress, RetrieveOperation::Create, - Some(wallet_addr.as_bytes()), + Some(wallet_addr.as_slice()), ) .expect("Should create account"); diff --git a/tests/parser_tests.rs b/tests/parser_tests.rs index cc9279f..46b7963 100644 --- a/tests/parser_tests.rs +++ b/tests/parser_tests.rs @@ -1,7 +1,43 @@ -use ethers_core::abi::{encode, AbiParser, FixedBytes, Token}; -use ethers_core::types::{Address, U256}; -use ethers_core::utils::id; +use alloy_dyn_abi::DynSolValue; +use alloy_primitives::{keccak256, Address, U256}; use json::JsonValue; + +// --- Test-local ABI oracle over alloy. The parser was migrated off the EOL ethers-rs; +// these helpers rebuild calldata the same standard-ABI way to drive the decode round-trips +// and voucher checks (byte-for-byte standard ABI encoding via alloy). --- +type FixedBytes = Vec; + +enum Token { + Uint(U256), + Address(Address), + Bytes(Vec), + FixedBytes(FixedBytes), +} + +fn token_to_dyn(t: &Token) -> DynSolValue { + match t { + Token::Uint(v) => DynSolValue::Uint(*v, 256), + Token::Address(a) => DynSolValue::Address(*a), + Token::Bytes(b) => DynSolValue::Bytes(b.clone()), + Token::FixedBytes(b) => DynSolValue::FixedBytes( + alloy_primitives::FixedBytes::<32>::right_padding_from(b), + 32, + ), + } +} + +fn encode(tokens: &[Token]) -> Vec { + DynSolValue::Tuple(tokens.iter().map(token_to_dyn).collect()).abi_encode_params() +} + +fn id(signature: &str) -> [u8; 4] { + // ethers' `AbiParser` normalized the signature (e.g. stripped whitespace) before + // hashing; mirror that so a test signature like "f(uint256, bytes)" yields the same + // 4-byte selector as the canonical "f(uint256,bytes)". + let normalized: String = signature.chars().filter(|c| !c.is_whitespace()).collect(); + let h = keccak256(normalized.as_bytes()); + [h[0], h[1], h[2], h[3]] +} use libcma_binding_rust::parser::{ cma_decode_advance, cma_decode_inspect, cma_encode_voucher, CmaParserErc20VoucherFields, CmaParserErc721VoucherFields, CmaParserEtherVoucherFields, CmaParserInputData, @@ -28,8 +64,8 @@ pub fn abi_encode_call( signature: &str, args: Vec, ) -> Result, Box> { - let function = AbiParser::default().parse_function(signature)?; - let calldata = function.encode_input(&args)?; + let mut calldata = id(signature)[..4].to_vec(); + calldata.extend_from_slice(&encode(&args)); Ok(calldata) } @@ -38,7 +74,7 @@ fn test_ether_deposit_success() { let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" .parse() .unwrap(); - let amount = U256::from_dec_str("2000000000000000000").unwrap(); // 2 Ether in wei + let amount = U256::from_str_radix("2000000000000000000", 10).unwrap(); // 2 Ether in wei let payload = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000001bc16d674ec80000".to_string(); // Sample payload from the ether portal let input = create_test_input(ETHER_PORTAL, &payload); @@ -74,7 +110,7 @@ fn test_ether_deposit_success() { // let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" // .parse() // .unwrap(); -// let amount = U256::from_dec_str("300000000000000000000").unwrap(); +// let amount = U256::from_str_radix("300000000000000000000", 10).unwrap(); // let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" // .parse() // .unwrap(); @@ -112,7 +148,7 @@ fn test_erc20_deposit_success() { let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" .parse() .unwrap(); - let amount = U256::from_dec_str("300000000000000000000").unwrap(); + let amount = U256::from_str_radix("300000000000000000000", 10).unwrap(); let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" .parse() .unwrap(); @@ -143,7 +179,7 @@ fn test_erc721_deposit_success() { let sender: Address = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266" .parse() .unwrap(); - let token_id = U256::from_dec_str("1").unwrap(); // Sample token ID + let token_id = U256::from_str_radix("1", 10).unwrap(); // Sample token ID let token_address: Address = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B" .parse() .unwrap(); // Sample ERC721 token address @@ -181,8 +217,8 @@ fn test_erc721_deposit_success() { #[test] fn test_ethers_withdrawal_success() { let recipient = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"; - let amount = U256::from_dec_str("1500000000000000000").unwrap(); // 1.5 Ether in wei - // let payload = r#"{"function_type": "EtherWithdrawal", "amount": "1500000000000000000", "exec_layer_data": "0x"}"#.to_string(); + let amount = U256::from_str_radix("1500000000000000000", 10).unwrap(); // 1.5 Ether in wei + // let payload = r#"{"function_type": "EtherWithdrawal", "amount": "1500000000000000000", "exec_layer_data": "0x"}"#.to_string(); let abi_encoded_input = abi_encode_call( "WithdrawEther(uint256, bytes)", @@ -197,22 +233,22 @@ fn test_ethers_withdrawal_success() { Ok(result) => { let is_correct_method = result.req_type == CmaParserInputType::CmaParserInputTypeEtherWithdrawal; - let is_correct_recipient = if let CmaParserInputData::EtherWithdrawal(withdrawal) = - result.input - { - if withdrawal.amount == amount && withdrawal.receiver == recipient.parse().unwrap() - { - println!( - "withdrawal response is {}, {}", - withdrawal.amount, withdrawal.receiver - ); - true + let is_correct_recipient = + if let CmaParserInputData::EtherWithdrawal(withdrawal) = result.input { + if withdrawal.amount == amount + && withdrawal.receiver == recipient.parse::
().unwrap() + { + println!( + "withdrawal response is {}, {}", + withdrawal.amount, withdrawal.receiver + ); + true + } else { + false + } } else { false - } - } else { - false - }; + }; assert_eq!(true, is_correct_method, "Expected Ether Withdrawal method"); assert_eq!( @@ -227,7 +263,7 @@ fn test_ethers_withdrawal_success() { #[test] fn test_erc20_withdrawal_success() { let recipient = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"; - let amount = U256::from_dec_str("50000000000000000000").unwrap(); // 50 ERC20 tokens in wei + let amount = U256::from_str_radix("50000000000000000000", 10).unwrap(); // 50 ERC20 tokens in wei let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" .parse() .unwrap(); // TEST token address @@ -251,7 +287,7 @@ fn test_erc20_withdrawal_success() { let is_correct_recipient = if let CmaParserInputData::Erc20Withdrawal(withdrawal) = result.input { if withdrawal.amount == amount - && withdrawal.receiver == recipient.parse().unwrap() + && withdrawal.receiver == recipient.parse::
().unwrap() && withdrawal.token == token_address { true @@ -275,7 +311,7 @@ fn test_erc20_withdrawal_success() { #[test] fn test_erc721_withdrawal_success() { let recipient = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"; - let token_id = U256::from_dec_str("1").unwrap(); // Sample token ID + let token_id = U256::from_str_radix("1", 10).unwrap(); // Sample token ID let token_address: Address = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B" .parse() .unwrap(); // Sample ERC721 token address @@ -299,7 +335,7 @@ fn test_erc721_withdrawal_success() { let is_correct_recipient = if let CmaParserInputData::Erc721Withdrawal(withdrawal) = result.input { if withdrawal.token_id == token_id - && withdrawal.receiver == recipient.parse().unwrap() + && withdrawal.receiver == recipient.parse::
().unwrap() && withdrawal.token == token_address { true @@ -327,9 +363,9 @@ fn test_ether_transfer_success() { recipient_bytes[31] = 120; let recipient: FixedBytes = FixedBytes::from(recipient_bytes); - let expected_receipient: U256 = U256::from_big_endian(&recipient); + let expected_receipient: U256 = U256::from_be_slice(&recipient); - let amount = U256::from_dec_str("1500000000000000000").unwrap(); // 1.5 Ether in wei + let amount = U256::from_str_radix("1500000000000000000", 10).unwrap(); // 1.5 Ether in wei let abi_encoded_input = abi_encode_call( "TransferEther(bytes32,uint256,bytes)", @@ -379,9 +415,9 @@ fn test_erc20_transfer_success() { recipient_bytes[31] = 120; let recipient: FixedBytes = FixedBytes::from(recipient_bytes); - let expected_receipient: U256 = U256::from_big_endian(&recipient); + let expected_receipient: U256 = U256::from_be_slice(&recipient); - let amount = U256::from_dec_str("1500000000000000000").unwrap(); // 1.5 Ether in wei + let amount = U256::from_str_radix("1500000000000000000", 10).unwrap(); // 1.5 Ether in wei let abi_encoded_input = abi_encode_call( "TransferErc20(address,bytes32,uint256,bytes)", @@ -425,7 +461,7 @@ fn test_erc20_transfer_success() { #[test] fn test_erc721_transfer_success() { - let token_id = U256::from_dec_str("1").unwrap(); // Sample token ID + let token_id = U256::from_str_radix("1", 10).unwrap(); // Sample token ID let sender = "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"; let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" .parse() @@ -435,7 +471,7 @@ fn test_erc721_transfer_success() { recipient_bytes[31] = 120; let recipient: FixedBytes = FixedBytes::from(recipient_bytes); - let expected_receipient: U256 = U256::from_big_endian(&recipient); + let expected_receipient: U256 = U256::from_be_slice(&recipient); let abi_encoded_input = abi_encode_call( "TransferErc721(address,bytes32,uint256,bytes)", @@ -483,16 +519,19 @@ fn test_ether_voucher_encoding_success() { let recipient: Address = "0x3e157927fb178490941bb18adcdc4144e442e32a" .parse() .unwrap(); - let amount = U256::from_dec_str("1500000000000000000").unwrap(); // 1.5 Ether in wei - let mut expected_value_bytes = [0u8; 32]; - amount.to_big_endian(&mut expected_value_bytes); + let amount = U256::from_str_radix("1500000000000000000", 10).unwrap(); // 1.5 Ether in wei + let expected_value_bytes = amount.to_be_bytes::<32>(); let request = CmaVoucherFieldType::EtherVoucherFields(CmaParserEtherVoucherFields { receiver: recipient, amount, }); - match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeEther, None, request) { + match cma_encode_voucher( + CmaParserVoucherType::CmaParserVoucherTypeEther, + None, + request, + ) { Ok(voucher) => { assert!( voucher.destination.to_lowercase() == receipient_string, @@ -521,7 +560,7 @@ fn test_erc20_voucher_encoding_success() { let token_address: Address = "0xFBdB734EF6a23aD76863CbA6f10d0C5CBBD8342C" .parse() .unwrap(); // TEST token address - let amount = U256::from_dec_str("50000000000000000000").unwrap(); // 50 ERC20 tokens in wei + let amount = U256::from_str_radix("50000000000000000000", 10).unwrap(); // 50 ERC20 tokens in wei // Create expected voucher payload let args: Vec = vec![Token::Address(recipient), Token::Uint(amount)]; @@ -540,7 +579,11 @@ fn test_erc20_voucher_encoding_success() { amount, receiver: recipient, }); - match cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeErc20, None, request) { + match cma_encode_voucher( + CmaParserVoucherType::CmaParserVoucherTypeErc20, + None, + request, + ) { Ok(voucher) => { // Basic checks on the voucher structure assert!( @@ -568,7 +611,7 @@ fn test_erc721_voucher_encoding_success() { let token_address: Address = "0xBa46623aD94AB45850c4ecbA9555D26328917c3B" .parse() .unwrap(); // Sample ERC721 token address - let token_id = U256::from_dec_str("1").unwrap(); // Sample token ID + let token_id = U256::from_str_radix("1", 10).unwrap(); // Sample token ID // Create expected voucher payload let args: Vec = vec![ @@ -632,8 +675,8 @@ fn test_ledger_get_balance_success() { result.req_type == CmaParserInputType::CmaParserInputTypeBalance; let is_correct_address = if let CmaParserInputData::Balance(data) = result.input { let mut expected_account = [0u8; 32]; - expected_account[12..].copy_from_slice(address.as_bytes()); - data.account == U256::from_big_endian(&expected_account) + expected_account[12..].copy_from_slice(address.as_slice()); + data.account == U256::from_be_slice(&expected_account) && data.token == erc20_token && data.token_id == U256::from(1) } else { diff --git a/tests/parser_vectors.rs b/tests/parser_vectors.rs index 4e33948..5e19763 100644 --- a/tests/parser_vectors.rs +++ b/tests/parser_vectors.rs @@ -1,6 +1,6 @@ //! Canonical parser vectors ported from `third_party/machine-asset-tools/tests/parser.c`. -use ethers_core::types::{Address, U256}; +use alloy_primitives::{Address, U256}; use json::JsonValue; use libcma_binding_rust::parser::{ cma_decode_advance, cma_decode_inspect, cma_encode_voucher, CmaParserErc721VoucherFields, @@ -99,7 +99,8 @@ fn vector_ether_transfer_from_parser_c() { #[test] fn vector_ledger_get_balance_from_parser_c() { - let payload = r#"{"method":"ledger_getBalance","params":["0x0000000000000000000000000000000000000001"]}"#; + let payload = + r#"{"method":"ledger_getBalance","params":["0x0000000000000000000000000000000000000001"]}"#; let result = cma_decode_inspect(inspect_input(payload)).expect("ledger_getBalance should decode"); @@ -115,9 +116,8 @@ fn vector_erc721_voucher_from_parser_c() { let app_address = Address::from_slice(&[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xbe, 0xef, ]); - let receiver = Address::from_slice(&[ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, - ]); + let receiver = + Address::from_slice(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]); let token = Address::from_slice(&[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, ]); @@ -145,20 +145,18 @@ fn vector_ether_voucher_value_from_parser_c() { let receiver: Address = "0x3e157927fb178490941bb18adcdc4144e442e32a" .parse() .unwrap(); - let amount = U256::from_dec_str("1500000000000000000").unwrap(); - let request = CmaVoucherFieldType::EtherVoucherFields(CmaParserEtherVoucherFields { - receiver, - amount, - }); + let amount = U256::from_str_radix("1500000000000000000", 10).unwrap(); + let request = + CmaVoucherFieldType::EtherVoucherFields(CmaParserEtherVoucherFields { receiver, amount }); - let voucher = cma_encode_voucher(CmaParserVoucherType::CmaParserVoucherTypeEther, None, request) - .expect("ether voucher should encode"); + let voucher = cma_encode_voucher( + CmaParserVoucherType::CmaParserVoucherTypeEther, + None, + request, + ) + .expect("ether voucher should encode"); - let mut expected_value = [0u8; 32]; - amount.to_big_endian(&mut expected_value); - assert_eq!( - voucher.value, - format!("0x{}", hex::encode(expected_value)) - ); + let expected_value = amount.to_be_bytes::<32>(); + assert_eq!(voucher.value, format!("0x{}", hex::encode(expected_value))); assert_eq!(voucher.payload, "0x"); } From 95cfecd478b6cd95f6d2ea2729f316cf4523aff8 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Fri, 24 Jul 2026 22:43:27 +0100 Subject: [PATCH 5/6] deps: adopt libcma uint96 single-asset format (drive format v2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump the vendored submodules to the upstream libcma update: - machine-asset-tools 1288499 -> 19a1e5e (feat: uint96 single asset, PR #4) - machine-guest-tools 3d838a2 -> 20eba47 (libcmt struct-name / ffi.h fixes) What changed upstream (machine-asset-tools e4bfc24): the single-asset ledger's withdrawable drive record widened its balance from uint64 to uint96, consuming the former 4-byte pad. The 32-byte record is now balance_lo (u64 LE) | balance_hi (u32 LE) | owner (20B) so the owner moved from offset 8 to offset 12 and there is no trailing pad. Total supply and virtual (internal account-id) balances widened to full 256-bit, and the drive MemoryFooter VERSION bumped 1 -> 2 (a v1 drive is NOT compatible). Impact on this crate is minimal: the public C API is unchanged (deposits, withdrawals, balances and total supply already cross the FFI as 256-bit cma_amount_t), so the Rust wrapper surface (src/) needs no changes — only code that reads the raw 32-byte records is affected. - tests/host_real_records_layout.rs: decode the uint96 balance (lo|hi) and read the owner at offset 12; drop the pad assertion; document format v2. - Doc comments (src/ledger.rs, src/lib.rs, README.md) updated to the v2 layout. - CHANGELOG notes the format bump and that downstream raw-record readers (a sequencer's create_dump/snapshot parser, the emergency-withdrawal output builder) MUST adopt the offset-12 owner + uint96 balance. Verified: host-real 97 tests green (records layout confirms lo=balance, hi=0, owner@12), mock 91 green, cargo fmt clean. --- CHANGELOG.md | 13 ++++++++ README.md | 5 +-- src/ledger.rs | 3 +- src/lib.rs | 3 +- tests/host_real_records_layout.rs | 52 +++++++++++++++++++------------ third_party/machine-asset-tools | 2 +- third_party/machine-guest-tools | 2 +- 7 files changed, 54 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f4381..1dec807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,19 @@ While the crate is pre-1.0 (`0.0.x`), any release may contain breaking changes. `SECURITY.md`, `deny.toml`, `rust-toolchain.toml`, and `rustfmt.toml`. ### Changed +- **Vendored `libcma` bumped to the uint96 single-asset format (drive format v2; + machine-asset-tools `e4bfc24`).** The single-asset drive record widened its + balance from `uint64` to `uint96`, consuming the former 4-byte pad: the 32-byte + record is now `balance_lo (u64 LE) | balance_hi (u32 LE) | owner (20B)`, with the + owner moved from offset 8 to **offset 12**. Total supply and virtual (internal + account-id) balances widened to full 256-bit. The public C API (and therefore the + Rust wrapper surface) is UNCHANGED — deposits/withdrawals/balances already used + 256-bit `cma_amount_t` at the boundary; only code that parses the raw 32-byte + records must adopt the new offsets. **The on-drive format is not backward + compatible** (`MemoryFooter::VERSION` 1 → 2): a v1 drive would be silently + misread. Downstream that reads the records image directly (e.g. a sequencer's + `create_dump` / snapshot parser and the emergency-withdrawal output builder) MUST + be updated to the offset-12 owner and uint96 balance. - **BREAKING: reshaped the ledger API around a single-asset ledger.** Removed `LedgerMemoryMode` and reshaped `LedgerFileConfig`. Code that constructed a ledger via the old memory-mode / file-config shape must be updated. diff --git a/README.md b/README.md index 8ed3e92..3da7e68 100644 --- a/README.md +++ b/README.md @@ -85,8 +85,9 @@ its message; the fix is to disable default features. `host-real` and `riscv64` compile the C++ `libcma` with SIMD-free / generic flags (`-DBOOST_UNORDERED_DISABLE_SSE2`, `-DBOOST_UNORDERED_DISABLE_NEON`, `-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION`). This makes the on-disk 32-byte -account records (`balance` u64 little-endian | `owner` 20 bytes | 4 bytes -padding) **byte-identical** across x86_64 and riscv64. That invariant is what +account records (single-asset drive format v2: `balance` uint96 little-endian +[low u64 | high u32] | `owner` 20 bytes, no padding) **byte-identical** across +x86_64 and riscv64. That invariant is what makes off-chain prediction with `host-real` sound: the host reproduces, byte for byte, exactly what the machine computes on-chain. diff --git a/src/ledger.rs b/src/ledger.rs index 75ecbc3..de3d259 100644 --- a/src/ledger.rs +++ b/src/ledger.rs @@ -18,7 +18,8 @@ //! `host-real` and `riscv64` build libcma with SIMD-free / generic flags //! (`-DBOOST_UNORDERED_DISABLE_SSE2`, `-DBOOST_UNORDERED_DISABLE_NEON`, //! `-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION`) so the on-disk 32-byte account -//! records (`balance` u64 little-endian | `owner` 20 bytes | 4 bytes padding) are +//! records (single-asset drive format v2: `balance` uint96 little-endian [low u64 | +//! high u32] | `owner` 20 bytes, no padding) are //! byte-identical across x86_64 and riscv64. This is what makes off-chain //! prediction with `host-real` sound: the host reproduces, byte for byte, //! exactly what the machine computes on-chain. diff --git a/src/lib.rs b/src/lib.rs index 99dc93c..d88f070 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,8 @@ //! `host-real` and `riscv64` compile libcma with SIMD-free / generic flags //! (`-DBOOST_UNORDERED_DISABLE_SSE2`, `-DBOOST_UNORDERED_DISABLE_NEON`, //! `-DBOOST_INTERPROCESS_FORCE_GENERIC_EMULATION`) so the on-disk 32-byte account -//! records (`balance` u64 little-endian | `owner` 20 bytes | 4 bytes padding) are +//! records (single-asset drive format v2: `balance` uint96 little-endian [low u64 | +//! high u32] | `owner` 20 bytes, no padding) are //! byte-identical across x86_64 and riscv64. That invariant is what makes //! off-chain prediction with `host-real` sound: the host reproduces, byte for //! byte, exactly what the machine computes on-chain. diff --git a/tests/host_real_records_layout.rs b/tests/host_real_records_layout.rs index 0a70aa4..b3cfe25 100644 --- a/tests/host_real_records_layout.rs +++ b/tests/host_real_records_layout.rs @@ -15,13 +15,20 @@ //! `third_party/machine-asset-tools/src/ledger_impl.h`: //! //! ```c +//! // Drive record format v2 (machine-asset-tools e4bfc24): the withdrawable balance was +//! // widened from uint64 to uint96, consuming the former padding. The owner therefore moved +//! // from offset 8 to offset 12, and there is no trailing pad. //! struct alignas(32) cma_ledger_single_balance { -//! uint64_t balance; // bytes 0..8 little-endian (interprocess.hpp asserts LE) -//! cma_abi_address_t address; // bytes 8..28 20-byte owner wallet address -//! uint8_t padding[4]; // bytes 28..32 zero pad to a 32-byte power-of-two record +//! uint64_t balance_lo; // bytes 0..8 low 64 bits, little-endian +//! uint32_t balance_hi; // bytes 8..12 high 32 bits, little-endian (uint96 = hi<<64 | lo) +//! cma_abi_address_t address; // bytes 12..32 20-byte owner wallet address //! }; //! static_assert(sizeof(cma_ledger_single_balance) == 32); +//! static_assert(offsetof(cma_ledger_single_balance, address) == 12); //! ``` +//! +//! NOTE (drive format v2): `MemoryFooter::VERSION` is now 2 — a v1 drive (uint64 balance, owner +//! at offset 8, 4-byte pad) is NOT compatible and would be silently misread by this format. #![cfg(feature = "host-real")] use libcma_binding_rust::ledger::{Ledger, LedgerAsset}; @@ -31,11 +38,18 @@ const MEM_LEN: usize = 4 * 1024 * 1024; const MAX_ACCOUNTS: usize = 4096; const RECORDS_PREFIX: usize = MAX_ACCOUNTS * 32; // 128 KiB proven records region -// Record byte layout (see module docs / ledger_impl.h). These offsets are the invariant. +// Record byte layout v2 (see module docs / ledger_impl.h). These offsets are the invariant. const RECORD_SIZE: usize = 32; -const BALANCE_RANGE: std::ops::Range = 0..8; // u64 little-endian -const OWNER_RANGE: std::ops::Range = 8..28; // 20-byte owner address -const PAD_RANGE: std::ops::Range = 28..32; // 4-byte zero pad +const BALANCE_LO_RANGE: std::ops::Range = 0..8; // low 64 bits, little-endian +const BALANCE_HI_RANGE: std::ops::Range = 8..12; // high 32 bits, little-endian +const OWNER_RANGE: std::ops::Range = 12..32; // 20-byte owner address + +/// Decode the uint96 balance from a record: `hi << 64 | lo` (both little-endian). +fn record_balance(rec: &[u8]) -> u128 { + let lo = u64::from_le_bytes(rec[BALANCE_LO_RANGE].try_into().unwrap()) as u128; + let hi = u32::from_le_bytes(rec[BALANCE_HI_RANGE].try_into().unwrap()) as u128; + (hi << 64) | lo +} fn token() -> Address { "0x88A2120B7068E78692C8fd12E751d610B6377E4d" @@ -72,7 +86,7 @@ fn build_ledger(buf: &mut [u8], credits: &[(Address, u64)]) { } } -/// Return the 32-byte record in `buf`'s records prefix whose owner field (bytes 8..28) +/// Return the 32-byte record in `buf`'s records prefix whose owner field (bytes 12..32) /// equals `owner`, or `None` if no such record exists. fn find_record(buf: &[u8], owner: Address) -> Option<&[u8]> { let want = owner.0 .0; @@ -81,9 +95,10 @@ fn find_record(buf: &[u8], owner: Address) -> Option<&[u8]> { .find(|rec| rec[OWNER_RANGE] == want) } -/// Each credited account's 32-byte record is exactly `balance(u64 LE) | owner(20) | pad(4)`. +/// Each credited account's 32-byte record is exactly `balance_lo(u64 LE) | balance_hi(u32 LE) | +/// owner(20)` — a uint96 balance followed by the 20-byte owner, with no trailing pad (format v2). #[test] -fn record_layout_is_balance_owner_pad() { +fn record_layout_is_balance96_and_owner() { let credits = [(alice(), 250u64), (bob(), 70u64)]; let mut buf = vec![0u8; MEM_LEN]; @@ -95,20 +110,17 @@ fn record_layout_is_balance_owner_pad() { }); eprintln!("record for {owner:?} = {}", hex::encode(rec)); - // balance: u64, little-endian, bytes 0..8 - let got = u64::from_le_bytes(rec[BALANCE_RANGE].try_into().unwrap()); - assert_eq!(got, bal, "balance field (u64 LE) mismatch for {owner:?}"); - - // owner: 20-byte address, bytes 8..28 + // balance: uint96, little-endian (low 64 bits bytes 0..8, high 32 bits bytes 8..12) assert_eq!( - rec[OWNER_RANGE], owner.0 .0, - "owner field (bytes 8..28) mismatch for {owner:?}" + record_balance(rec), + bal as u128, + "balance field (uint96 LE) mismatch for {owner:?}" ); - // pad: 4 zero bytes, bytes 28..32 + // owner: 20-byte address, bytes 12..32 assert_eq!( - rec[PAD_RANGE], [0u8; 4], - "trailing pad (bytes 28..32) must be zero for {owner:?}" + rec[OWNER_RANGE], owner.0 .0, + "owner field (bytes 12..32) mismatch for {owner:?}" ); } } diff --git a/third_party/machine-asset-tools b/third_party/machine-asset-tools index 1288499..19a1e5e 160000 --- a/third_party/machine-asset-tools +++ b/third_party/machine-asset-tools @@ -1 +1 @@ -Subproject commit 1288499afa1643cfd692383888b762a7ede3b6b4 +Subproject commit 19a1e5efa599622f4bebbab6bb15faf29aa82b2d diff --git a/third_party/machine-guest-tools b/third_party/machine-guest-tools index 3d838a2..20eba47 160000 --- a/third_party/machine-guest-tools +++ b/third_party/machine-guest-tools @@ -1 +1 @@ -Subproject commit 3d838a2f80cd8fa614e38ea93efcfabe8ec65bea +Subproject commit 20eba47631a1b0938c6e0f2c4af1ac88a172c90d From 77745e0b2a5e89b53c96e37b1682d388677c77b3 Mon Sep 17 00:00:00 2001 From: Idogwu Chinonso Date: Sun, 26 Jul 2026 22:31:14 +0100 Subject: [PATCH 6/6] fix(build): build only libcma's own third-party deps, skip cartesi-machine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `build.rs` ran the blanket `make third-party`, which — after the v2 Makefile change — also downloads and extracts the prebuilt cartesi-machine emulator .deb (~57 MB, xz-compressed). That library is linked ONLY by the host-side `account-driver-reader` tool, never by `libcma.a`, so pulling it in is pure overhead: it needs `xz` and network for tens of MB of nothing, and it breaks minimal build environments — notably the Cartesi machine riscv64 cross-build image, which ships no `xz` (`tar (grandchild): xz: Cannot exec` -> build fails). Build only the three sub-targets libcma.a needs — `third-party-boost`, `third-party-guest-tools`, `third-party-nlohmann-json`. The `.o` compile rules have no cartesi-machine prerequisite, and `make libcma` no longer triggers it. Verified: host-real build + tests still green; unblocks the riscv64 machine cross-build (Docker) that consumes the crate. --- build.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 315d583..95ee99c 100644 --- a/build.rs +++ b/build.rs @@ -154,10 +154,19 @@ fn main() { // Download + stage the third-party deps, then compile the static archive. The Makefile // hardcodes `libcma_OBJDIR := build/riscv64`; override it so the host build lands in its // own dir (command-line assignments beat the Makefile's `:=`). + // + // Build ONLY the pieces libcma.a actually needs — Boost, the guest-tools libcmt + // headers, and nlohmann/json. The blanket `make third-party` also downloads and + // extracts the prebuilt cartesi-machine emulator .deb (~57 MB, xz-compressed), which is + // linked ONLY by the host-side `account-driver-reader` tool, NOT by libcma.a. Pulling it + // in needs `xz` (and downloads tens of MB) for nothing, and breaks minimal build + // environments such as the Cartesi machine cross-build image (which ships no `xz`). run( "make", &[ - "third-party", + "third-party-boost", + "third-party-guest-tools", + "third-party-nlohmann-json", &format!("TOOLCHAIN_PREFIX={toolchain_prefix}"), ], &mat,