Skip to content

Repository files navigation



Cartesi Rollups LIBCMA Binding for Rust

libcma-binding-rust

LIBCMA is a lightweight Rust binding for Cartesi Machine Application (CMA) tooling: parse rollup inputs, build on-chain voucher payloads, and manage application assets (deposits, withdrawals, transfers, balances) for ETH, ERC-20, ERC-721, and ERC-1155.

Clone with submodules

Headers come from git submodules under third_party/:

  • third_party/machine-asset-toolslibcma (parser, types, ledger)
  • third_party/machine-guest-toolslibcmt headers used by libcma
git clone --recurse-submodules https://github.com/Mugen-Builders/libcma_binding_rust
# or, if already cloned:
git submodule update --init --recursive

Build requirements

A plain cargo build works out of the box — build.rs takes care of the fiddly parts:

  • Submodules auto-init. If third_party/ is empty (you cloned without --recurse-submodules), build.rs runs git submodule update --init --recursive for you.
  • bindgen clang headers auto-fallback. bindgen needs the compiler's builtin headers (stdbool.h, …). If your libclang ships without them (e.g. you have libclang1 but not libclang-common-*-dev), build.rs falls back to GCC's builtin header dir automatically — no BINDGEN_EXTRA_CLANG_ARGS needed. Set that env var yourself to override.

So the only hard host requirement for the default build is:

  • A Rust toolchain + libclang (for bindgen) and git (for the submodule fetch).

For the riscv64 build (linking the real C++ libcma), build.rs also cross-compiles the static archive from source if it isn't already present. That path additionally needs:

  • GNU make, wget, and network access
  • The RISC-V GCC 14 cross toolchaing++-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.

The Cartesi SDK / app Docker image used to build the machine already provides all of these.

Feature flags

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:

# 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:

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.

riscv64: your application must supply libcmt

libcma.a contains only libcma's own objects. Its parser references libcmt's C ABI helpers (cmt_abi_*, cmt_buf_*), so build.rs emits -lcmt for the riscv64 backend and libcmt.a must be on the linker's search path in the stage that cross-links your binary. Upstream deliberately does not build libcmt for riscv64 — its real io backend needs the Cartesi Linux kernel headers, which exist only inside the guest — so it comes from the released machine-guest-tools package. (host-real needs nothing extra: build.rs builds and statically links libcmt.a itself, from the vendored sources with the mock io backend.)

Installing machine-guest-tools in your application's runtime stage is not enough — by then the binary is already linked. Stage it into the cross sysroot of the build stage, using the same guest-tools version your runtime stage installs, so you link and run against one libcmt ABI:

# in the cross-build stage, before `cargo build`
ARG MACHINE_GUEST_TOOLS_VERSION
ADD https://github.com/cartesi/machine-guest-tools/releases/download/v${MACHINE_GUEST_TOOLS_VERSION}/machine-guest-tools_riscv64.deb /tmp/gt.deb
RUN dpkg-deb -x /tmp/gt.deb /tmp/gt && \
    cp -a /tmp/gt/usr/lib/libcmt.a     /usr/riscv64-linux-gnu/lib/ && \
    cp -a /tmp/gt/usr/include/libcmt   /usr/riscv64-linux-gnu/include/

# libcma is C++20/23, so it needs GCC >= 14; the stock template installs 13.
ENV CMA_RISCV64_CXX=riscv64-linux-gnu-g++-14 \
    CMA_RISCV64_CC=riscv64-linux-gnu-gcc-14

Symptom if this is missing: the C++ compile succeeds and the failure appears only at the final link, as undefined symbol: cmt_abi_get_uint, cmt_buf_split, and friends. It surfaces only once something calls the C parser bindings, because static archive members are pulled lazily.

+crt-static and the C++ runtime

The Cartesi Rust template builds with -C target-feature=+crt-static, and that flag is honoured for riscv64gc-unknown-linux-gnu — glibc is linked in statically. A pure-Rust application therefore produces a fully static binary with no PT_INTERP at all, which is what the machine's rootfs expects.

Pulling in a C++ library changes that. If libstdc++ were linked dynamically, the binary would keep a single NEEDED libstdc++.so.6 and, with it, an interpreter of /lib/ld.so.1 — a path that does not exist in the machine rootfs (Ubuntu riscv64 installs the loader as /lib/ld-linux-riscv64-lp64d.so.1). The machine then cannot exec the application and reports only:

WARN rollup_http_server::dapp_process] throwing exception because dapp failed to
     start with No such file or directory (os error 2)

which names neither the loader nor libstdc++.

So when +crt-static is set, build.rs binds libstdc++ and libcmt statically as well, locating each archive through <compiler> -print-file-name= so the paths come from the toolchain rather than being guessed. Nothing is required of the application beyond providing a C++ toolchain that ships libstdc++.a — the g++-14-riscv64-linux-gnu package does. Verify with:

$ riscv64-linux-gnu-readelf -l dapp | grep -i interpreter   # expect no output
$ riscv64-linux-gnu-readelf -d dapp | grep NEEDED           # expect no output

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 (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.

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.

  • retrieve_ether_assets() uses AssetType::Base
  • AssetType also supports TokenAddress, TokenAddressId, and TokenAddressIdAmount
  • RetrieveOperation::FindAndRemove is supported

Parser and vouchers

Pure-Rust parser aligned with machine-asset-tools / Cartesi Rollups v2.0:

  • Portal deposit decoding (packed + ABI tails for ERC-721/1155)
  • Auto-decode withdrawals/transfers by function selector
  • Inspect decoding for ledger_getBalance and ledger_getTotalSupply
  • Voucher encoding for Ether, ERC-20, ERC-721 (safeTransferFrom), ERC-1155 single/batch (safeTransferFrom / safeBatchTransferFrom)

Core public functions

  • cma_decode_advance(req_type, input) -> Result<CmaParserInput, CmaParserError>
  • cma_decode_inspect(input) -> Result<CmaParserInput, CmaParserError>
  • cma_encode_voucher(req_type, app_address, voucher_request) -> Result<CmaVoucher, CmaParserError>

CmaVoucher fields: destination, value (wei for ether vouchers), payload.

Inspect params are flat JSON strings, e.g.:

{"method":"ledger_getBalance","params":["0x...account...","0x...token...","0x1"]}

Tests

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 the mock backend

CI runs the mock-backend tests on every push/PR and attempts an riscv64 link check when libcma can be built.

License

MIT (see Cargo.toml).

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages