From e8f660c2c7588729e9e5bc290db06793284af566 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Wed, 8 Oct 2025 20:27:09 -0300 Subject: [PATCH 01/13] wip --- nssa/Cargo.toml | 3 +++ nssa/build.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++ nssa/src/lib.rs | 4 +++ nssa/src/program.rs | 2 +- 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 nssa/build.rs diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index 93553a8..f1a055e 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -15,6 +15,9 @@ rand = "0.8" borsh = "1.5.7" hex = "0.4.3" +[build-dependencies] +risc0-build = "3.0.3" + [dev-dependencies] test-program-methods = { path = "test_program_methods" } hex-literal = "1.0.0" diff --git a/nssa/build.rs b/nssa/build.rs new file mode 100644 index 0000000..f04ba25 --- /dev/null +++ b/nssa/build.rs @@ -0,0 +1,66 @@ +use std::{env, fs, path::Path, process::Command}; +use risc0_build::a; + +fn main() { + // 1️⃣ Crate root and OUT_DIR + let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let out_dir = env::var("OUT_DIR").unwrap(); + + // 2️⃣ Directory to write generated module + let mod_dir = Path::new(&out_dir).join("nssa_programs"); + let mod_file = mod_dir.join("mod.rs"); + + println!("cargo:rerun-if-changed=program_methods/guest"); + + // 3️⃣ Build the Risc0 guest program + let guest_manifest = Path::new(&manifest_dir) + .join("program_methods/guest/Cargo.toml"); + + let status = Command::new("cargo") + .arg("risczero") + .arg("build") + .arg("--manifest-path") + .arg(&guest_manifest) + .status() + .expect("failed to run risczero build"); + assert!(status.success(), "Risc0 deterministic build failed"); + + // 4️⃣ Target directory where the Risc0 build produces .bin files + let target_dir = Path::new(&manifest_dir) + .join("program_methods/guest/target/riscv32im-risc0-zkvm-elf/docker/"); + + println!("cargo:warning=Looking for binaries in {}", target_dir.display()); + + // 5️⃣ Collect all .bin files + let bins = fs::read_dir(&target_dir) + .expect("failed to read external target dir") + .filter_map(Result::ok) + .filter(|e| e.path().extension().map(|ext| ext == "bin").unwrap_or(false)) + .collect::>(); + + if bins.is_empty() { + panic!("No .bin files found in {:?}", target_dir); + } + + println!("cargo:warning=Found {} binaries:", bins.len()); + for b in &bins { + println!("cargo:warning= - {}", b.path().display()); + } + + // 6️⃣ Generate Rust module + fs::create_dir_all(&mod_dir).unwrap(); + let mut src = String::new(); + for entry in bins { + let path = entry.path(); + let name = path.file_stem().unwrap().to_string_lossy(); + src.push_str(&format!( + "pub const {}_ELF: &[u8] = include_bytes!(r#\"{}\"#);\n", + name.to_uppercase(), + path.display() + )); + } + + fs::write(&mod_file, src).unwrap(); + println!("cargo:warning=Generated module at {}", mod_file.display()); +} + diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 21defa9..cdf45a2 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -1,3 +1,7 @@ +pub mod programs { + include!(concat!(env!("OUT_DIR"), "/nssa_programs/mod.rs")); +} + pub mod encoding; pub mod error; mod merkle_tree; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index 5abc153..fc13865 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -2,7 +2,7 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{InstructionData, ProgramId, ProgramOutput}, }; -use program_methods::{ +use crate::programs::{ AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, }; From 69b610269b6b708ee8739accae5c5d6a6584f2a4 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Thu, 9 Oct 2025 21:27:43 -0300 Subject: [PATCH 02/13] use risc0 method for image_id computaiton --- ci_scripts/test-ubuntu.sh | 10 +-- nssa/Cargo.toml | 4 +- nssa/build.rs | 77 +++++++++---------- nssa/src/lib.rs | 8 +- .../privacy_preserving_transaction/circuit.rs | 2 +- nssa/src/program.rs | 2 +- 6 files changed, 53 insertions(+), 50 deletions(-) diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index 5c19b36..b563e6b 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -4,16 +4,16 @@ curl -L https://risczero.com/install | bash /home/runner/.risc0/bin/rzup install source env.sh -RISC0_DEV_MODE=1 cargo test --release +RISC0_DEV_MODE=1 cargo test --release --features no_docker cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -cargo run $(pwd)/configs/debug all +cargo run $(pwd)/configs/debug all --features no_docker echo "Try test valid proof at least once" -cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account +cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account --features no_docker echo "Continuing in dev mode" -RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all +RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all --features no_docker cd .. -cd nssa/program_methods/guest && cargo test --release +cd nssa/program_methods/guest && cargo test --release --features no_docker diff --git a/nssa/Cargo.toml b/nssa/Cargo.toml index f1a055e..a86bd98 100644 --- a/nssa/Cargo.toml +++ b/nssa/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" thiserror = "2.0.12" risc0-zkvm = { version = "3.0.3", features = ['std'] } nssa-core = { path = "core", features = ["host"] } -program-methods = { path = "program_methods" } +program-methods = { path = "program_methods", optional = true } serde = "1.0.219" sha2 = "0.10.9" secp256k1 = "0.31.1" @@ -17,6 +17,7 @@ hex = "0.4.3" [build-dependencies] risc0-build = "3.0.3" +risc0-binfmt = "3.0.2" [dev-dependencies] test-program-methods = { path = "test_program_methods" } @@ -24,3 +25,4 @@ hex-literal = "1.0.0" [features] default = [] +no_docker = ["program-methods"] diff --git a/nssa/build.rs b/nssa/build.rs index f04ba25..cc4608e 100644 --- a/nssa/build.rs +++ b/nssa/build.rs @@ -1,66 +1,63 @@ -use std::{env, fs, path::Path, process::Command}; -use risc0_build::a; - fn main() { - // 1️⃣ Crate root and OUT_DIR - let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let out_dir = env::var("OUT_DIR").unwrap(); + if cfg!(feature = "no_docker") { + println!("cargo:warning=NO_DOCKER feature enabled – deterministic build skipped"); + return; + } - // 2️⃣ Directory to write generated module - let mod_dir = Path::new(&out_dir).join("nssa_programs"); + build_deterministic().expect("Deterministic build failed"); +} + +fn build_deterministic() -> Result<(), Box> { + use std::{env, fs, path::PathBuf, process::Command}; + + let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?); + let out_dir = PathBuf::from(env::var("OUT_DIR")?); + let mod_dir = out_dir.join("program_methods"); let mod_file = mod_dir.join("mod.rs"); - println!("cargo:rerun-if-changed=program_methods/guest"); + println!("cargo:rerun-if-changed=program_methods/guest/src"); + println!("cargo:rerun-if-changed=program_methods/guest/Cargo.toml"); - // 3️⃣ Build the Risc0 guest program - let guest_manifest = Path::new(&manifest_dir) - .join("program_methods/guest/Cargo.toml"); + let guest_manifest = manifest_dir.join("program_methods/guest/Cargo.toml"); let status = Command::new("cargo") - .arg("risczero") - .arg("build") - .arg("--manifest-path") + .args(["risczero", "build", "--manifest-path"]) .arg(&guest_manifest) - .status() - .expect("failed to run risczero build"); - assert!(status.success(), "Risc0 deterministic build failed"); + .status()?; + if !status.success() { + return Err("Risc0 deterministic build failed".into()); + } - // 4️⃣ Target directory where the Risc0 build produces .bin files - let target_dir = Path::new(&manifest_dir) - .join("program_methods/guest/target/riscv32im-risc0-zkvm-elf/docker/"); + let target_dir = + manifest_dir.join("program_methods/guest/target/riscv32im-risc0-zkvm-elf/docker/"); - println!("cargo:warning=Looking for binaries in {}", target_dir.display()); - - // 5️⃣ Collect all .bin files - let bins = fs::read_dir(&target_dir) - .expect("failed to read external target dir") + let bins = fs::read_dir(&target_dir)? .filter_map(Result::ok) - .filter(|e| e.path().extension().map(|ext| ext == "bin").unwrap_or(false)) + .filter(|e| e.path().extension().is_some_and(|ext| ext == "bin")) .collect::>(); if bins.is_empty() { - panic!("No .bin files found in {:?}", target_dir); + return Err(format!("No .bin files found in {:?}", target_dir).into()); } - println!("cargo:warning=Found {} binaries:", bins.len()); - for b in &bins { - println!("cargo:warning= - {}", b.path().display()); - } - - // 6️⃣ Generate Rust module - fs::create_dir_all(&mod_dir).unwrap(); + fs::create_dir_all(&mod_dir)?; let mut src = String::new(); for entry in bins { let path = entry.path(); let name = path.file_stem().unwrap().to_string_lossy(); + let bytecode = fs::read(&path)?; + let image_id: [u32; 8] = risc0_binfmt::compute_image_id(&bytecode)?.into(); src.push_str(&format!( - "pub const {}_ELF: &[u8] = include_bytes!(r#\"{}\"#);\n", + "pub const {}_ELF: &[u8] = include_bytes!(r#\"{}\"#);\n\ + pub const {}_ID: [u32; 8] = {:?};\n", name.to_uppercase(), - path.display() + path.display(), + name.to_uppercase(), + image_id )); } - - fs::write(&mod_file, src).unwrap(); + fs::write(&mod_file, src)?; println!("cargo:warning=Generated module at {}", mod_file.display()); -} + Ok(()) +} diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index cdf45a2..5227875 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -1,7 +1,11 @@ -pub mod programs { - include!(concat!(env!("OUT_DIR"), "/nssa_programs/mod.rs")); +#[cfg(not(feature = "no_docker"))] +pub mod program_methods { + include!(concat!(env!("OUT_DIR"), "/program_methods/mod.rs")); } +#[cfg(feature = "no_docker")] +use program_methods; + pub mod encoding; pub mod error; mod merkle_tree; diff --git a/nssa/src/privacy_preserving_transaction/circuit.rs b/nssa/src/privacy_preserving_transaction/circuit.rs index 3a98723..9ce0610 100644 --- a/nssa/src/privacy_preserving_transaction/circuit.rs +++ b/nssa/src/privacy_preserving_transaction/circuit.rs @@ -8,7 +8,7 @@ use risc0_zkvm::{ExecutorEnv, InnerReceipt, Receipt, default_prover}; use crate::{error::NssaError, program::Program}; -use program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; +use crate::program_methods::{PRIVACY_PRESERVING_CIRCUIT_ELF, PRIVACY_PRESERVING_CIRCUIT_ID}; /// Proof of the privacy preserving execution circuit #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/nssa/src/program.rs b/nssa/src/program.rs index fc13865..b229241 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -2,7 +2,7 @@ use nssa_core::{ account::{Account, AccountWithMetadata}, program::{InstructionData, ProgramId, ProgramOutput}, }; -use crate::programs::{ +use crate::program_methods::{ AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, }; From f200f39779d57c568804628d57295df322a36a8c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 10 Oct 2025 16:48:12 -0300 Subject: [PATCH 03/13] minor fix --- ci_scripts/test-ubuntu.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index b563e6b..65d219e 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -9,11 +9,11 @@ RISC0_DEV_MODE=1 cargo test --release --features no_docker cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -cargo run $(pwd)/configs/debug all --features no_docker +cargo run --features no_docker $(pwd)/configs/debug all echo "Try test valid proof at least once" -cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account --features no_docker +cargo run --features no_docker $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account echo "Continuing in dev mode" -RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all --features no_docker +RISC0_DEV_MODE=1 cargo run --features no_docker $(pwd)/configs/debug all cd .. cd nssa/program_methods/guest && cargo test --release --features no_docker From 766d72bd754bf11201dbfec3daa85154b25694ca Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 10 Oct 2025 17:44:42 -0300 Subject: [PATCH 04/13] remove get programs --- common/src/rpc_primitives/requests.rs | 12 +++++++++++ common/src/sequencer_client/mod.rs | 27 ++++++++++++++++++++++-- nssa/src/lib.rs | 1 + sequencer_rpc/src/process.rs | 30 +++++++++++++++++++++++---- wallet/src/lib.rs | 30 ++++++++++++++++++++++++++- 5 files changed, 93 insertions(+), 7 deletions(-) diff --git a/common/src/rpc_primitives/requests.rs b/common/src/rpc_primitives/requests.rs index 94c2ddc..91596e6 100644 --- a/common/src/rpc_primitives/requests.rs +++ b/common/src/rpc_primitives/requests.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use crate::parse_request; use super::errors::RpcParseError; use super::parser::RpcRequest; use super::parser::parse_params; +use nssa_core::program::ProgramId; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -58,6 +61,9 @@ pub struct GetProofForCommitmentRequest { pub commitment: nssa_core::Commitment, } +#[derive(Serialize, Deserialize, Debug)] +pub struct GetProgramIdsRequest {} + parse_request!(HelloRequest); parse_request!(RegisterAccountRequest); parse_request!(SendTxRequest); @@ -70,6 +76,7 @@ parse_request!(GetTransactionByHashRequest); parse_request!(GetAccountsNoncesRequest); parse_request!(GetProofForCommitmentRequest); parse_request!(GetAccountRequest); +parse_request!(GetProgramIdsRequest); #[derive(Serialize, Deserialize, Debug)] pub struct HelloResponse { @@ -126,3 +133,8 @@ pub struct GetAccountResponse { pub struct GetProofForCommitmentResponse { pub membership_proof: Option, } + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetProgramIdsResponse { + pub program_ids: HashMap, +} diff --git a/common/src/sequencer_client/mod.rs b/common/src/sequencer_client/mod.rs index 1aec903..2d1bc4c 100644 --- a/common/src/sequencer_client/mod.rs +++ b/common/src/sequencer_client/mod.rs @@ -1,16 +1,19 @@ +use std::collections::HashMap; + use super::rpc_primitives::requests::{ GetAccountBalanceRequest, GetAccountBalanceResponse, GetBlockDataRequest, GetBlockDataResponse, GetGenesisIdRequest, GetGenesisIdResponse, GetInitialTestnetAccountsRequest, }; use anyhow::Result; use json::{SendTxRequest, SendTxResponse, SequencerRpcRequest, SequencerRpcResponse}; +use nssa_core::program::ProgramId; use reqwest::Client; use serde_json::Value; use crate::rpc_primitives::requests::{ GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, - GetProofForCommitmentRequest, GetProofForCommitmentResponse, GetTransactionByHashRequest, - GetTransactionByHashResponse, + GetProgramIdsRequest, GetProgramIdsResponse, GetProofForCommitmentRequest, + GetProofForCommitmentResponse, GetTransactionByHashRequest, GetTransactionByHashResponse, }; use crate::sequencer_client::json::AccountInitialData; use crate::transaction::{EncodedTransaction, NSSATransaction}; @@ -237,4 +240,24 @@ impl SequencerClient { Ok(resp_deser) } + + // Get Ids of the programs used by the node + pub async fn get_program_ids( + &self, + ) -> Result, SequencerClientError> { + let acc_req = GetProgramIdsRequest {}; + + let req = serde_json::to_value(acc_req).unwrap(); + + let resp = self + .call_method_with_payload("get_program_ids", req) + .await + .unwrap(); + + let resp_deser = serde_json::from_value::(resp) + .unwrap() + .program_ids; + + Ok(resp_deser) + } } diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 21defa9..296cb2a 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -7,6 +7,7 @@ pub mod public_transaction; mod signature; mod state; +pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; pub use nssa_core::account::{Account, AccountId}; pub use nssa_core::address::Address; pub use privacy_preserving_transaction::{ diff --git a/sequencer_rpc/src/process.rs b/sequencer_rpc/src/process.rs index f376f94..d8c3c01 100644 --- a/sequencer_rpc/src/process.rs +++ b/sequencer_rpc/src/process.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use actix_web::Error as HttpError; use base64::{Engine, engine::general_purpose}; -use nssa; +use nssa::{self, program::Program}; use sequencer_core::config::AccountInitialData; use serde_json::Value; @@ -14,9 +16,9 @@ use common::{ requests::{ GetAccountBalanceRequest, GetAccountBalanceResponse, GetAccountRequest, GetAccountResponse, GetAccountsNoncesRequest, GetAccountsNoncesResponse, - GetInitialTestnetAccountsRequest, GetProofForCommitmentRequest, - GetProofForCommitmentResponse, GetTransactionByHashRequest, - GetTransactionByHashResponse, + GetInitialTestnetAccountsRequest, GetProgramIdsRequest, GetProgramIdsResponse, + GetProofForCommitmentRequest, GetProofForCommitmentResponse, + GetTransactionByHashRequest, GetTransactionByHashResponse, }, }, transaction::EncodedTransaction, @@ -40,6 +42,7 @@ pub const GET_TRANSACTION_BY_HASH: &str = "get_transaction_by_hash"; pub const GET_ACCOUNTS_NONCES: &str = "get_accounts_nonces"; pub const GET_ACCOUNT: &str = "get_account"; pub const GET_PROOF_FOR_COMMITMENT: &str = "get_proof_for_commitment"; +pub const GET_PROGRAM_IDS: &str = "get_program_ids"; pub const HELLO_FROM_SEQUENCER: &str = "HELLO_FROM_SEQUENCER"; @@ -267,6 +270,24 @@ impl JsonHandler { respond(helperstruct) } + async fn process_get_program_ids(&self, request: Request) -> Result { + let _get_proof_req = GetProgramIdsRequest::parse(Some(request.params))?; + + let mut program_ids = HashMap::new(); + program_ids.insert( + "authenticated_transfer".to_string(), + Program::authenticated_transfer_program().id(), + ); + program_ids.insert("token".to_string(), Program::token().id()); + program_ids.insert("pinata".to_string(), Program::pinata().id()); + program_ids.insert( + "privacy_preserving_circuit".to_string(), + nssa::PRIVACY_PRESERVING_CIRCUIT_ID, + ); + let helperstruct = GetProgramIdsResponse { program_ids }; + respond(helperstruct) + } + pub async fn process_request_internal(&self, request: Request) -> Result { match request.method.as_ref() { HELLO => self.process_temp_hello(request).await, @@ -280,6 +301,7 @@ impl JsonHandler { GET_ACCOUNT => self.process_get_account(request).await, GET_TRANSACTION_BY_HASH => self.process_get_transaction_by_hash(request).await, GET_PROOF_FOR_COMMITMENT => self.process_get_proof_by_commitment(request).await, + GET_PROGRAM_IDS => self.process_get_program_ids(request).await, _ => Err(RpcErr(RpcError::method_not_found(request.method))), } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 0e8b1cb..12505ff 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -11,7 +11,7 @@ use anyhow::Result; use chain_storage::WalletChainStore; use config::WalletConfig; use log::info; -use nssa::{Account, Address}; +use nssa::{program::Program, Account, Address}; use clap::{Parser, Subcommand}; use nssa_core::Commitment; @@ -377,6 +377,9 @@ pub enum Command { #[arg(long)] solution: u128, }, + // Check the wallet can connect to the node and builtin local programs + // match the remote versions + CheckHealth { } } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -845,6 +848,31 @@ pub async fn execute_subcommand(command: Command) -> Result { + let remote_program_ids = wallet_core.sequencer_client.get_program_ids().await.expect("Error fetching program ids"); + let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer") else { + panic!("Missing authenticated transfer ID from remote"); + }; + if authenticated_transfer_id != &Program::authenticated_transfer_program().id() { + panic!("Local ID for authenticated transfer program is different from remote"); + } + let Some(token_id) = remote_program_ids.get("token") else { + panic!("Missing token program ID from remote"); + }; + if token_id != &Program::token().id() { + panic!("Local ID for token program is different from remote"); + } + let Some(circuit_id) = remote_program_ids.get("privacy_preserving_circuit") else { + panic!("Missing privacy preserving circuit ID from remote"); + }; + if circuit_id != &nssa::PRIVACY_PRESERVING_CIRCUIT_ID { + panic!("Local ID for privacy preserving circuit is different from remote"); + } + + println!("✅All looks good!"); + SubcommandReturnValue::Empty } }; From d7240e073a961889b47702df671f5f97a0c36797 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 10 Oct 2025 17:51:22 -0300 Subject: [PATCH 05/13] fix integration tests --- ci_scripts/test-ubuntu.sh | 8 ++++---- integration_tests/Cargo.toml | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index 65d219e..d62cbb3 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -9,11 +9,11 @@ RISC0_DEV_MODE=1 cargo test --release --features no_docker cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -cargo run --features no_docker $(pwd)/configs/debug all +cargo run $(pwd)/configs/debug all echo "Try test valid proof at least once" -cargo run --features no_docker $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account +cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account echo "Continuing in dev mode" -RISC0_DEV_MODE=1 cargo run --features no_docker $(pwd)/configs/debug all +RISC0_DEV_MODE=1 cargo run $(pwd)/configs/debug all cd .. -cd nssa/program_methods/guest && cargo test --release --features no_docker +cd nssa/program_methods/guest && cargo test --release diff --git a/integration_tests/Cargo.toml b/integration_tests/Cargo.toml index db2dfeb..f7bd131 100644 --- a/integration_tests/Cargo.toml +++ b/integration_tests/Cargo.toml @@ -36,3 +36,4 @@ path = "../common" [dependencies.nssa] path = "../nssa" +features = ["no_docker"] From 35806f103615c3a1c493c3909d45f3455c546503 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 10 Oct 2025 18:24:11 -0300 Subject: [PATCH 06/13] fmt --- nssa/src/lib.rs | 2 +- wallet/src/lib.rs | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 296cb2a..0dd4ff2 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -7,12 +7,12 @@ pub mod public_transaction; mod signature; mod state; -pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; pub use nssa_core::account::{Account, AccountId}; pub use nssa_core::address::Address; pub use privacy_preserving_transaction::{ PrivacyPreservingTransaction, circuit::execute_and_prove, }; +pub use program_methods::PRIVACY_PRESERVING_CIRCUIT_ID; pub use public_transaction::PublicTransaction; pub use signature::PrivateKey; pub use signature::PublicKey; diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 12505ff..e56aea6 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -11,7 +11,7 @@ use anyhow::Result; use chain_storage::WalletChainStore; use config::WalletConfig; use log::info; -use nssa::{program::Program, Account, Address}; +use nssa::{Account, Address, program::Program}; use clap::{Parser, Subcommand}; use nssa_core::Commitment; @@ -379,7 +379,7 @@ pub enum Command { }, // Check the wallet can connect to the node and builtin local programs // match the remote versions - CheckHealth { } + CheckHealth {}, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -850,9 +850,14 @@ pub async fn execute_subcommand(command: Command) -> Result { - let remote_program_ids = wallet_core.sequencer_client.get_program_ids().await.expect("Error fetching program ids"); - let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer") else { + Command::CheckHealth {} => { + let remote_program_ids = wallet_core + .sequencer_client + .get_program_ids() + .await + .expect("Error fetching program ids"); + let Some(authenticated_transfer_id) = remote_program_ids.get("authenticated_transfer") + else { panic!("Missing authenticated transfer ID from remote"); }; if authenticated_transfer_id != &Program::authenticated_transfer_program().id() { From ba35fafad4c2c65cdc0819f618336dee586b447c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 13 Oct 2025 15:09:50 -0300 Subject: [PATCH 07/13] fmt --- nssa/src/lib.rs | 1 + nssa/src/program.rs | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/nssa/src/lib.rs b/nssa/src/lib.rs index 5227875..a635c0f 100644 --- a/nssa/src/lib.rs +++ b/nssa/src/lib.rs @@ -4,6 +4,7 @@ pub mod program_methods { } #[cfg(feature = "no_docker")] +#[allow(clippy::single_component_path_imports)] use program_methods; pub mod encoding; diff --git a/nssa/src/program.rs b/nssa/src/program.rs index b229241..f6df8fe 100644 --- a/nssa/src/program.rs +++ b/nssa/src/program.rs @@ -1,11 +1,11 @@ -use nssa_core::{ - account::{Account, AccountWithMetadata}, - program::{InstructionData, ProgramId, ProgramOutput}, -}; use crate::program_methods::{ AUTHENTICATED_TRANSFER_ELF, AUTHENTICATED_TRANSFER_ID, PINATA_ELF, PINATA_ID, TOKEN_ELF, TOKEN_ID, }; +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{InstructionData, ProgramId, ProgramOutput}, +}; use risc0_zkvm::{ExecutorEnv, ExecutorEnvBuilder, default_executor, serde::to_vec}; use serde::Serialize; From cf9d296d085bcd6bf02f06f6707185c909f5200c Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 13 Oct 2025 15:26:14 -0300 Subject: [PATCH 08/13] remove redundant test in ci --- ci_scripts/test-ubuntu.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/ci_scripts/test-ubuntu.sh b/ci_scripts/test-ubuntu.sh index d62cbb3..2aeba86 100755 --- a/ci_scripts/test-ubuntu.sh +++ b/ci_scripts/test-ubuntu.sh @@ -9,7 +9,6 @@ RISC0_DEV_MODE=1 cargo test --release --features no_docker cd integration_tests export NSSA_WALLET_HOME_DIR=$(pwd)/configs/debug/wallet/ export RUST_LOG=info -cargo run $(pwd)/configs/debug all echo "Try test valid proof at least once" cargo run $(pwd)/configs/debug test_success_private_transfer_to_another_owned_account echo "Continuing in dev mode" From 6274addad92a75511fd99a47b0db658274c151d0 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 13 Oct 2025 16:25:59 -0300 Subject: [PATCH 09/13] Remove unused Nonce alias --- common/src/transaction.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 3a2bda1..5332e5c 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -5,12 +5,6 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, digest::FixedOutput}; -use elliptic_curve::{ - consts::{B0, B1}, - generic_array::GenericArray, -}; -use sha2::digest::typenum::{UInt, UTerm}; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum NSSATransaction { Public(nssa::PublicTransaction), @@ -32,7 +26,6 @@ impl From for NSSATransaction { use crate::TreeHashType; pub type CipherText = Vec; -pub type Nonce = GenericArray, B1>, B0>, B0>>; pub type Tag = u8; #[derive( From c772f1b0fe67c56e60739a5a89290f6959e5a880 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Fri, 10 Oct 2025 17:47:23 -0300 Subject: [PATCH 10/13] add init function --- integration_tests/src/lib.rs | 60 +++++++++++-------- .../guest/src/bin/authenticated_transfer.rs | 53 ++++++++++++---- wallet/src/lib.rs | 20 +++++++ wallet/src/token_transfers/public.rs | 26 ++++++++ 4 files changed, 123 insertions(+), 36 deletions(-) diff --git a/integration_tests/src/lib.rs b/integration_tests/src/lib.rs index 1690445..5ca3dab 100644 --- a/integration_tests/src/lib.rs +++ b/integration_tests/src/lib.rs @@ -791,6 +791,36 @@ pub async fn test_pinata() { info!("Success!"); } +pub async fn test_authenticated_transfer_initialize_function() { + info!("test initialize account for authenticated transfer"); + let command = Command::AuthenticatedTransferInitializePublicAccount {}; + + let SubcommandReturnValue::RegisterAccount { addr } = + wallet::execute_subcommand(command).await.unwrap() + else { + panic!("Error creating account"); + }; + + info!("Checking correct execution"); + let wallet_config = fetch_config().unwrap(); + let seq_client = SequencerClient::new(wallet_config.sequencer_addr.clone()).unwrap(); + let account = seq_client + .get_account(addr.to_string()) + .await + .unwrap() + .account; + + let expected_program_owner = Program::authenticated_transfer_program().id(); + let expected_nonce = 1; + let expected_balance = 0; + + assert_eq!(account.program_owner, expected_program_owner); + assert_eq!(account.balance, expected_balance); + assert_eq!(account.nonce, expected_nonce); + assert!(account.data.is_empty()); + info!("Success!"); +} + macro_rules! test_cleanup_wrap { ($home_dir:ident, $test_func:ident) => {{ let res = pre_test($home_dir.clone()).await.unwrap(); @@ -871,6 +901,9 @@ pub async fn main_tests_runner() -> Result<()> { "test_pinata" => { test_cleanup_wrap!(home_dir, test_pinata); } + "test_authenticated_transfer_initialize_function" => { + test_cleanup_wrap!(home_dir, test_authenticated_transfer_initialize_function); + } "all" => { test_cleanup_wrap!(home_dir, test_success_move_to_another_account); test_cleanup_wrap!(home_dir, test_success); @@ -902,32 +935,7 @@ pub async fn main_tests_runner() -> Result<()> { test_success_private_transfer_to_another_owned_account_claiming_path ); test_cleanup_wrap!(home_dir, test_pinata); - } - "all_private" => { - test_cleanup_wrap!( - home_dir, - test_success_private_transfer_to_another_owned_account - ); - test_cleanup_wrap!( - home_dir, - test_success_private_transfer_to_another_foreign_account - ); - test_cleanup_wrap!( - home_dir, - test_success_deshielded_transfer_to_another_account - ); - test_cleanup_wrap!( - home_dir, - test_success_shielded_transfer_to_another_owned_account - ); - test_cleanup_wrap!( - home_dir, - test_success_shielded_transfer_to_another_foreign_account - ); - test_cleanup_wrap!( - home_dir, - test_success_private_transfer_to_another_owned_account_claiming_path - ); + test_cleanup_wrap!(home_dir, test_authenticated_transfer_initialize_function); } _ => { anyhow::bail!("Unknown test name"); diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 210e3f0..31d606f 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -1,15 +1,32 @@ -use nssa_core::program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}; +use nssa_core::{ + account::{Account, AccountWithMetadata}, + program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, +}; -/// A transfer of balance program. -/// To be used both in public and private contexts. -fn main() { - // Read input accounts. - // It is expected to receive only two accounts: [sender_account, receiver_account] - let ProgramInput { - pre_states, - instruction: balance_to_move, - } = read_nssa_inputs(); +fn initialize_account(pre_states: Vec) { + // Continue only if input_accounts is an array of one element + let [pre_state] = match pre_states.try_into() { + Ok(array) => array, + Err(_) => return, + }; + let account_to_claim = pre_state.account.clone(); + let is_authorized = pre_state.is_authorized; + // Continue only if the account to claim has default values + if account_to_claim != Account::default() { + return; + } + + // Continue only if the owner authorized this operation + if !is_authorized { + return; + } + + // Noop will result in account being claimed for this program + write_nssa_outputs(vec![pre_state], vec![account_to_claim]); +} + +fn transfer(pre_states: Vec, balance_to_move: u128) { // Continue only if input_accounts is an array of two elements let [sender, receiver] = match pre_states.try_into() { Ok(array) => array, @@ -35,3 +52,19 @@ fn main() { write_nssa_outputs(vec![sender, receiver], vec![sender_post, receiver_post]); } +/// A transfer of balance program. +/// To be used both in public and private contexts. +fn main() { + // Read input accounts. + // It is expected to receive only two accounts: [sender_account, receiver_account] + let ProgramInput { + pre_states, + instruction: balance_to_move, + } = read_nssa_inputs(); + + match (pre_states.len(), balance_to_move) { + (1, 0) => initialize_account(pre_states), + (2, balance_to_move) => transfer(pre_states, balance_to_move), + _ => panic!("Invalid parameters"), + } +} diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 0e8b1cb..4a32ddc 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -377,6 +377,7 @@ pub enum Command { #[arg(long)] solution: u128, }, + AuthenticatedTransferInitializePublicAccount {}, } ///To execute commands, env var NSSA_WALLET_HOME_DIR must be set into directory with config @@ -847,6 +848,25 @@ pub async fn execute_subcommand(command: Command) -> Result { + let addr = wallet_core.create_new_account_public(); + + println!("Generated new account with addr {addr}"); + + let path = wallet_core.store_persistent_accounts()?; + + println!("Stored persistent accounts at {path:#?}"); + + let res = wallet_core + .register_account_under_authenticated_transfers_programs(addr) + .await?; + + println!("Results of tx send is {res:#?}"); + + let _transfer_tx = wallet_core.poll_native_token_transfer(res.tx_hash).await?; + + SubcommandReturnValue::RegisterAccount { addr } + } }; Ok(subcommand_ret) diff --git a/wallet/src/token_transfers/public.rs b/wallet/src/token_transfers/public.rs index 22d50eb..3e6df44 100644 --- a/wallet/src/token_transfers/public.rs +++ b/wallet/src/token_transfers/public.rs @@ -42,4 +42,30 @@ impl WalletCore { Err(ExecutionFailureKind::InsufficientFundsError) } } + + pub async fn register_account_under_authenticated_transfers_programs( + &self, + from: Address, + ) -> Result { + let Ok(nonces) = self.get_accounts_nonces(vec![from]).await else { + return Err(ExecutionFailureKind::SequencerError); + }; + + let instruction: u128 = 0; + let addresses = vec![from]; + let program_id = Program::authenticated_transfer_program().id(); + let message = Message::try_new(program_id, addresses, nonces, instruction).unwrap(); + + let signing_key = self.storage.user_data.get_pub_account_signing_key(&from); + + let Some(signing_key) = signing_key else { + return Err(ExecutionFailureKind::KeyNotFoundError); + }; + + let witness_set = WitnessSet::for_message(&message, &[signing_key]); + + let tx = PublicTransaction::new(message, witness_set); + + Ok(self.sequencer_client.send_tx_public(tx).await?) + } } From a2b9aef1527aaf25db99a54d803ff75b36d17e40 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 13 Oct 2025 17:45:03 -0300 Subject: [PATCH 11/13] minor refactor --- .../guest/src/bin/authenticated_transfer.rs | 32 +++++++------------ wallet/src/lib.rs | 3 +- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index 31d606f..ccd0bec 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -3,12 +3,7 @@ use nssa_core::{ program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; -fn initialize_account(pre_states: Vec) { - // Continue only if input_accounts is an array of one element - let [pre_state] = match pre_states.try_into() { - Ok(array) => array, - Err(_) => return, - }; +fn initialize_account(pre_state: AccountWithMetadata) { let account_to_claim = pre_state.account.clone(); let is_authorized = pre_state.is_authorized; @@ -26,13 +21,7 @@ fn initialize_account(pre_states: Vec) { write_nssa_outputs(vec![pre_state], vec![account_to_claim]); } -fn transfer(pre_states: Vec, balance_to_move: u128) { - // Continue only if input_accounts is an array of two elements - let [sender, receiver] = match pre_states.try_into() { - Ok(array) => array, - Err(_) => return, - }; - +fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { // Continue only if the sender has authorized this operation if !sender.is_authorized { return; @@ -45,26 +34,27 @@ fn transfer(pre_states: Vec, balance_to_move: u128) { // Create accounts post states, with updated balances let mut sender_post = sender.account.clone(); - let mut receiver_post = receiver.account.clone(); + let mut recipient_post = recipient.account.clone(); sender_post.balance -= balance_to_move; - receiver_post.balance += balance_to_move; + recipient_post.balance += balance_to_move; - write_nssa_outputs(vec![sender, receiver], vec![sender_post, receiver_post]); + write_nssa_outputs(vec![sender, recipient], vec![sender_post, recipient_post]); } /// A transfer of balance program. /// To be used both in public and private contexts. fn main() { // Read input accounts. - // It is expected to receive only two accounts: [sender_account, receiver_account] let ProgramInput { pre_states, instruction: balance_to_move, } = read_nssa_inputs(); - match (pre_states.len(), balance_to_move) { - (1, 0) => initialize_account(pre_states), - (2, balance_to_move) => transfer(pre_states, balance_to_move), - _ => panic!("Invalid parameters"), + match (pre_states.as_slice(), balance_to_move) { + ([account_to_claim], 0) => initialize_account(account_to_claim.clone()), + ([sender, recipient], balance_to_move) => { + transfer(sender.clone(), recipient.clone(), balance_to_move) + } + _ => panic!("invalid params"), } } diff --git a/wallet/src/lib.rs b/wallet/src/lib.rs index 4a32ddc..e5472bc 100644 --- a/wallet/src/lib.rs +++ b/wallet/src/lib.rs @@ -789,7 +789,8 @@ pub async fn execute_subcommand(command: Command) -> Result { let addr: Address = addr.parse()?; if let Some(account) = wallet_core.get_account_private(&addr) { - println!("{}", serde_json::to_string(&account).unwrap()); + let account_hr: HumanReadableAccount = account.into(); + println!("{}", serde_json::to_string(&account_hr).unwrap()); } else { println!("Private account not found."); } From fb4bb02f1a29b6e7172b6a1432ada26bb63b3f30 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 13 Oct 2025 18:06:02 -0300 Subject: [PATCH 12/13] add docstrings --- nssa/program_methods/guest/src/bin/authenticated_transfer.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs index ccd0bec..df8a38e 100644 --- a/nssa/program_methods/guest/src/bin/authenticated_transfer.rs +++ b/nssa/program_methods/guest/src/bin/authenticated_transfer.rs @@ -3,6 +3,8 @@ use nssa_core::{ program::{ProgramInput, read_nssa_inputs, write_nssa_outputs}, }; +/// Initializes a default account under the ownership of this program. +/// This is achieved by a noop. fn initialize_account(pre_state: AccountWithMetadata) { let account_to_claim = pre_state.account.clone(); let is_authorized = pre_state.is_authorized; @@ -21,6 +23,7 @@ fn initialize_account(pre_state: AccountWithMetadata) { write_nssa_outputs(vec![pre_state], vec![account_to_claim]); } +/// Transfers `balance_to_move` native balance from `sender` to `recipient`. fn transfer(sender: AccountWithMetadata, recipient: AccountWithMetadata, balance_to_move: u128) { // Continue only if the sender has authorized this operation if !sender.is_authorized { From 9559fb4bf0332da4db64fa3f39e8254f63fd1cc5 Mon Sep 17 00:00:00 2001 From: Sergio Chouhy Date: Mon, 13 Oct 2025 19:05:08 -0300 Subject: [PATCH 13/13] remove unused Nonce type alias --- common/src/transaction.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/common/src/transaction.rs b/common/src/transaction.rs index 3a2bda1..5332e5c 100644 --- a/common/src/transaction.rs +++ b/common/src/transaction.rs @@ -5,12 +5,6 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, digest::FixedOutput}; -use elliptic_curve::{ - consts::{B0, B1}, - generic_array::GenericArray, -}; -use sha2::digest::typenum::{UInt, UTerm}; - #[derive(Debug, Clone, PartialEq, Eq)] pub enum NSSATransaction { Public(nssa::PublicTransaction), @@ -32,7 +26,6 @@ impl From for NSSATransaction { use crate::TreeHashType; pub type CipherText = Vec; -pub type Nonce = GenericArray, B1>, B0>, B0>>; pub type Tag = u8; #[derive(