From 6e9edf3ba4f8ed01ff7f4034350e0dc69c31283f Mon Sep 17 00:00:00 2001 From: agureev Date: Tue, 2 Jun 2026 19:28:58 +0400 Subject: [PATCH 1/2] Add rustler crate for the nifs --- Cargo.lock | 74 ++++++++++++++++-- Cargo.toml | 1 + arm_nif/Cargo.toml | 21 +++++ arm_nif/src/error.rs | 25 ++++++ arm_nif/src/lib.rs | 13 ++++ arm_nif/src/verifier.rs | 166 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 295 insertions(+), 5 deletions(-) create mode 100644 arm_nif/Cargo.toml create mode 100644 arm_nif/src/error.rs create mode 100644 arm_nif/src/lib.rs create mode 100644 arm_nif/src/verifier.rs diff --git a/Cargo.lock b/Cargo.lock index b2b308fc..20680cf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -177,7 +177,7 @@ checksum = "6d792e205ed3b72f795a8044c52877d2e6b6e9b1d13f431478121d8d4eaa9028" dependencies = [ "alloy-sol-macro-input", "const-hex", - "heck", + "heck 0.5.0", "indexmap 2.11.4", "proc-macro-error2", "proc-macro2", @@ -195,7 +195,7 @@ checksum = "0bd1247a8f90b465ef3f1207627547ec16940c35597875cdc09c49d58b19693c" dependencies = [ "const-hex", "dunce", - "heck", + "heck 0.5.0", "macro-string", "proc-macro2", "quote", @@ -655,6 +655,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "arm_nif" +version = "0.1.0" +dependencies = [ + "anoma-rm-risc0", + "bincode", + "rustler", + "thiserror 2.0.17", +] + [[package]] name = "arraydeque" version = "0.5.1" @@ -1540,7 +1550,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97af9b5f014e228b33e77d75ee0e6e87960124f0f4b16337b586a6bec91867b1" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "proc-macro2-diagnostics", ] @@ -2140,6 +2150,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -3457,7 +3473,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -4326,6 +4342,39 @@ dependencies = [ "windows-sys 0.61.1", ] +[[package]] +name = "rustler" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75d458f38f550976d0e4b347ca57241c192019777e46af7af73b27783287088" +dependencies = [ + "lazy_static", + "rustler_codegen", + "rustler_sys", +] + +[[package]] +name = "rustler_codegen" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd46408f51c0ca6a68dc36aa4f90e3554960bd1b7cc513e6ff2ccad7dd92aff" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "rustler_sys" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76ba8524729d7c9db2b3e80f2269d1fdef39b5a60624c33fd794797e69b558" +dependencies = [ + "regex", + "unreachable", +] + [[package]] name = "rustls" version = "0.23.32" @@ -4748,7 +4797,7 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.106", @@ -5255,6 +5304,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -5303,6 +5361,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 557d909a..85947a5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "arm", + "arm_nif", "arm_tests/arm_test_witness", "arm_tests/arm_test_app", "arm_gadgets", diff --git a/arm_nif/Cargo.toml b/arm_nif/Cargo.toml new file mode 100644 index 00000000..4348dca9 --- /dev/null +++ b/arm_nif/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "arm_nif" +version = "0.1.0" +edition = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[lib] +name = "arm_nif" +path = "src/lib.rs" +crate-type = ["cdylib"] + +[dependencies] +# Verifier-only: drop the default `prove` feature so the prover toolchain is not +# linked into the NIF .so. `transaction` pulls in the Transaction/Action API and +# the host-side `verify` path. +anoma-rm-risc0 = { path = "../arm", default-features = false, features = ["transaction"] } +rustler = "0.31" +# Match the arm crate's serialization (bincode 1.x, serde built in). +bincode = "1.3.3" +thiserror = "2.0.6" diff --git a/arm_nif/src/error.rs b/arm_nif/src/error.rs new file mode 100644 index 00000000..fc2eda90 --- /dev/null +++ b/arm_nif/src/error.rs @@ -0,0 +1,25 @@ +//! Error type bridging arm-risc0 / bincode failures to Erlang terms. + +use anoma_rm_risc0::error::ArmError; +use rustler::{Encoder, Env, Term}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ArmNifError { + #[error("bincode decode error: {0}")] + BincodeDecode(#[from] bincode::Error), + #[error("ARM error: {0}")] + Arm(#[from] ArmError), +} + +impl Encoder for ArmNifError { + fn encode<'a>(&self, env: Env<'a>) -> Term<'a> { + self.to_string().encode(env) + } +} + +impl From for rustler::Error { + fn from(e: ArmNifError) -> Self { + rustler::Error::Term(Box::new(e)) + } +} diff --git a/arm_nif/src/lib.rs b/arm_nif/src/lib.rs new file mode 100644 index 00000000..8a1d1790 --- /dev/null +++ b/arm_nif/src/lib.rs @@ -0,0 +1,13 @@ +mod error; +mod verifier; + +rustler::init!( + "Elixir.ArmRisc0.Verifier", + [ + verifier::verify_transaction, + verifier::verify_and_extract, + verifier::transaction_nullifiers, + verifier::transaction_commitments, + verifier::transaction_roots, + ] +); diff --git a/arm_nif/src/verifier.rs b/arm_nif/src/verifier.rs new file mode 100644 index 00000000..48a4071b --- /dev/null +++ b/arm_nif/src/verifier.rs @@ -0,0 +1,166 @@ +//! NIFs verifying and reading arm-risc0 resource machine transactions. +//! +//! Every entry point takes a bincode-encoded `anoma_rm_risc0::transaction::Transaction` +//! as an opaque binary + +use crate::error::ArmNifError; +use anoma_rm_risc0::{ + Digest, + action::Action, + compliance::ComplianceInstance, + error::ArmError, + logic_instance::{AppData, ExpirableBlob}, + transaction::Transaction, +}; +use bincode::Options; +use rustler::{Binary, Env, NifResult, OwnedBinary}; +use std::collections::BTreeSet; + +/// Upper bound on a decoded transaction +const MAX_TX_BYTES: u64 = 128 * 1024 * 1024; // 128 MiB + +/// Decode a bincode-encoded `Transaction` +fn decode_tx(tx_bytes: &[u8]) -> Result { + Ok(bincode::options() + .with_fixint_encoding() + .with_little_endian() + .with_limit(MAX_TX_BYTES) + .deserialize(tx_bytes)?) +} + +/// Copy bytes into a freshly-allocated Erlang binary +fn to_bin<'a>(env: Env<'a>, bytes: &[u8]) -> Binary<'a> { + let mut bin = OwnedBinary::new(bytes.len()).expect("OwnedBinary allocation failed"); + bin.as_mut_slice().copy_from_slice(bytes); + bin.release(env) +} + +/// A risc0 `Digest` as a fixed 32-byte array +fn digest32(d: &Digest) -> [u8; 32] { + d.as_bytes() + .try_into() + .expect("a risc0 Digest is always 32 bytes") +} + +/// Decode the transaction and collect every action's compliance instance +fn instances(tx_bytes: &[u8]) -> Result, ArmNifError> { + decode_tx(tx_bytes)? + .actions + .iter() + .map(|a| a.compliance_unit.get_instance().map_err(ArmNifError::from)) + .collect() +} + +/// The four payload categories (resource, discovery, external, application) as +/// `(blob_words, deletion_criterion)` lists +type AppDataBlobs = ( + Vec<(Vec, u32)>, + Vec<(Vec, u32)>, + Vec<(Vec, u32)>, + Vec<(Vec, u32)>, +); + +fn app_data_blobs(ad: &AppData) -> AppDataBlobs { + let conv = |bs: &[ExpirableBlob]| -> Vec<(Vec, u32)> { + bs.iter() + .map(|b| (b.blob.clone(), b.deletion_criterion)) + .collect() + }; + ( + conv(&ad.resource_payload), + conv(&ad.discovery_payload), + conv(&ad.external_payload), + conv(&ad.application_payload), + ) +} + +fn app_data_for_tag(action: &Action, tag: &Digest) -> Result { + let input = action + .logic_verifier_inputs + .iter() + .find(|i| &i.tag == tag) + .ok_or(ArmError::TagNotFound)?; + Ok(app_data_blobs(&input.app_data)) +} + +#[rustler::nif(schedule = "DirtyCpu")] +fn verify_transaction(tx_bytes: Binary) -> NifResult { + Ok(decode_tx(tx_bytes.as_slice())?.verify().is_ok()) +} + +/// Decode + verify a transaction in one pass and return the effects needed for +/// global checks and storage +#[rustler::nif(schedule = "DirtyCpu")] +fn verify_and_extract<'a>( + env: Env<'a>, + tx_bytes: Binary<'a>, +) -> NifResult<( + Vec<(Binary<'a>, AppDataBlobs)>, + Vec<(Binary<'a>, AppDataBlobs)>, + Vec>, +)> { + let tx = decode_tx(tx_bytes.as_slice())?; + + // `Transaction::verify` consumes `self`, so gather the effects from a borrow + // first, then verify and only return them if verification succeeds. + let mut consumed = Vec::new(); + let mut created = Vec::new(); + let mut roots: BTreeSet<[u8; 32]> = BTreeSet::new(); + for action in &tx.actions { + let ci = action + .compliance_unit + .get_instance() + .map_err(ArmNifError::from)?; + for c in &ci.consumed_publics { + let ad = app_data_for_tag(action, &c.resource_nullifier)?; + consumed.push((to_bin(env, c.resource_nullifier.as_bytes()), ad)); + roots.insert(digest32(&c.commitment_tree_root)); + } + for c in &ci.created_publics { + let ad = app_data_for_tag(action, &c.resource_commitment)?; + created.push((to_bin(env, c.resource_commitment.as_bytes()), ad)); + } + } + + tx.verify().map_err(ArmNifError::from)?; + + let roots = roots.iter().map(|r| to_bin(env, r)).collect(); + Ok((consumed, created, roots)) +} + +#[rustler::nif(schedule = "DirtyCpu")] +fn transaction_nullifiers<'a>(env: Env<'a>, tx_bytes: Binary<'a>) -> NifResult>> { + Ok(instances(tx_bytes.as_slice())? + .iter() + .flat_map(|ci| { + ci.consumed_publics + .iter() + .map(|c| to_bin(env, c.resource_nullifier.as_bytes())) + }) + .collect()) +} + +#[rustler::nif(schedule = "DirtyCpu")] +fn transaction_commitments<'a>(env: Env<'a>, tx_bytes: Binary<'a>) -> NifResult>> { + Ok(instances(tx_bytes.as_slice())? + .iter() + .flat_map(|ci| { + ci.created_publics + .iter() + .map(|c| to_bin(env, c.resource_commitment.as_bytes())) + }) + .collect()) +} + +#[rustler::nif(schedule = "DirtyCpu")] +fn transaction_roots<'a>(env: Env<'a>, tx_bytes: Binary<'a>) -> NifResult>> { + let roots: BTreeSet<[u8; 32]> = instances(tx_bytes.as_slice())? + .iter() + .flat_map(|ci| { + ci.consumed_publics + .iter() + .map(|c| digest32(&c.commitment_tree_root)) + }) + .collect(); + Ok(roots.iter().map(|r| to_bin(env, r)).collect()) +} From 6d6b381cd3587c00a921efaf888d7069a68fd5c2 Mon Sep 17 00:00:00 2001 From: agureev Date: Tue, 2 Jun 2026 19:29:06 +0400 Subject: [PATCH 2/2] Add the Elixir project for the nifs --- .gitignore | 11 +++++++- arm_nif/src/verifier.rs | 3 --- lib/arm_risc0/verifier.ex | 55 +++++++++++++++++++++++++++++++++++++++ mix.exs | 25 ++++++++++++++++++ mix.lock | 5 ++++ 5 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 lib/arm_risc0/verifier.ex create mode 100644 mix.exs create mode 100644 mix.lock diff --git a/.gitignore b/.gitignore index 53ff97ba..846ccec3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,13 @@ _build/ deps/ .vscode/ .idea/ -profile.* \ No newline at end of file +profile.* + +# Elixir / mix +*.ez +erl_crash.dump +/cover/ +/doc/ + +# rustler NIF build artifact (compiled into priv/native at `mix compile`) +*.so \ No newline at end of file diff --git a/arm_nif/src/verifier.rs b/arm_nif/src/verifier.rs index 48a4071b..527bef77 100644 --- a/arm_nif/src/verifier.rs +++ b/arm_nif/src/verifier.rs @@ -100,9 +100,6 @@ fn verify_and_extract<'a>( Vec>, )> { let tx = decode_tx(tx_bytes.as_slice())?; - - // `Transaction::verify` consumes `self`, so gather the effects from a borrow - // first, then verify and only return them if verification succeeds. let mut consumed = Vec::new(); let mut created = Vec::new(); let mut roots: BTreeSet<[u8; 32]> = BTreeSet::new(); diff --git a/lib/arm_risc0/verifier.ex b/lib/arm_risc0/verifier.ex new file mode 100644 index 00000000..125c04c4 --- /dev/null +++ b/lib/arm_risc0/verifier.ex @@ -0,0 +1,55 @@ +defmodule ArmRisc0.Verifier do + # `crate: :arm_nif` would default to `native/arm_nif/` relative to the mix + # project root. The crate lives at the repo root here, so point `path:` at it. + use Rustler, + otp_app: :arm_risc0, + crate: :arm_nif, + path: "arm_nif" + + @moduledoc """ + NIF bindings for verifying arm-risc0 resource machine transactions. + + All functions take a bincode-encoded `anoma_rm_risc0::transaction::Transaction` + as a binary. + """ + + @typedoc "Result type for NIF functions that can return errors" + @type nif_result(t) :: t | {:error, term()} + + @typedoc "A payload blob: its raw u32 words and deletion criterion." + @type blob :: {[non_neg_integer()], non_neg_integer()} + + @typedoc "The four payload categories: {resource, discovery, external, application}." + @type app_data_blobs :: {[blob()], [blob()], [blob()], [blob()]} + + @doc """ + Verify a bincode-encoded Transaction + """ + @spec verify_transaction(binary()) :: nif_result(boolean()) + def verify_transaction(_tx_bytes), do: error() + + @doc """ + Decode + verify a transaction in one pass and return its effects needed for + global checks and storage + """ + @spec verify_and_extract(binary()) :: + {[{<<_::256>>, app_data_blobs()}], [{<<_::256>>, app_data_blobs()}], [<<_::256>>]} + | {:error, term()} + def verify_and_extract(_tx_bytes), do: error() + + @doc "All nullifiers (32-byte binaries) in the transaction, in transaction order." + @spec transaction_nullifiers(binary()) :: nif_result(list(<<_::256>>)) + def transaction_nullifiers(_tx_bytes), do: error() + + @doc "All commitments (32-byte binaries) in the transaction, in transaction order." + @spec transaction_commitments(binary()) :: nif_result(list(<<_::256>>)) + def transaction_commitments(_tx_bytes), do: error() + + @doc """ + The set of consumed-resource roots in the transaction + """ + @spec transaction_roots(binary()) :: nif_result(list(<<_::256>>)) + def transaction_roots(_tx_bytes), do: error() + + defp error, do: :erlang.nif_error(:nif_not_loaded) +end diff --git a/mix.exs b/mix.exs new file mode 100644 index 00000000..ec2e0697 --- /dev/null +++ b/mix.exs @@ -0,0 +1,25 @@ +defmodule ArmRisc0.MixProject do + use Mix.Project + + def project do + [ + app: :arm_risc0, + version: "0.1.0", + elixir: "~> 1.17", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger] + ] + end + + defp deps do + [ + {:rustler, "~> 0.31.0"} + ] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 00000000..70a1f0e0 --- /dev/null +++ b/mix.lock @@ -0,0 +1,5 @@ +%{ + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, + "rustler": {:hex, :rustler, "0.31.0", "7e5eefe61e6e6f8901e5aa3de60073d360c6320d9ec363027b0197297b80c46a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "99e378459bfb9c3bda6d3548b2b3bc6f9ad97f728f76bdbae7bf5c770a4f8abd"}, + "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, +}