From bc6ba30f6620b8eb938142202983cbfe7385bf86 Mon Sep 17 00:00:00 2001 From: Pravdyvy Date: Thu, 21 May 2026 17:04:48 +0300 Subject: [PATCH] feat: generic public transactions --- Cargo.lock | 1 + wallet-ffi/Cargo.toml | 1 + wallet-ffi/src/account.rs | 28 ++- wallet-ffi/src/error.rs | 2 + wallet-ffi/src/generic_transaction.rs | 247 ++++++++++++++++++++++++++ wallet-ffi/src/lib.rs | 1 + wallet-ffi/wallet_ffi.h | 95 +++++++++- 7 files changed, 367 insertions(+), 8 deletions(-) create mode 100644 wallet-ffi/src/generic_transaction.rs diff --git a/Cargo.lock b/Cargo.lock index 52bb1ce8..91bbd3b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10240,6 +10240,7 @@ dependencies = [ "key_protocol", "nssa", "nssa_core", + "risc0-zkvm", "sequencer_service_rpc", "serde_json", "tempfile", diff --git a/wallet-ffi/Cargo.toml b/wallet-ffi/Cargo.toml index 869845c8..9940607f 100644 --- a/wallet-ffi/Cargo.toml +++ b/wallet-ffi/Cargo.toml @@ -19,6 +19,7 @@ sequencer_service_rpc = { workspace = true, features = ["client"] } tokio.workspace = true key_protocol.workspace = true serde_json.workspace = true +risc0-zkvm.workspace = true [build-dependencies] cbindgen = "0.29" diff --git a/wallet-ffi/src/account.rs b/wallet-ffi/src/account.rs index ed27abe6..b4fb1e84 100644 --- a/wallet-ffi/src/account.rs +++ b/wallet-ffi/src/account.rs @@ -14,7 +14,7 @@ use crate::{ WalletHandle, }, wallet::get_wallet, - FfiU128, + FfiAccountIdentity, FfiU128, }; /// Create a new public account. @@ -653,3 +653,29 @@ pub unsafe extern "C" fn wallet_ffi_import_private_account( } } } + +/// Free account identity returned by `wallet_ffi_resolve_private_account` or +/// `wallet_ffi_resolve_public_account`. +/// +/// # Safety +/// The account must be either null or a valid account returned by +/// `wallet_ffi_resolve_private_account` or `wallet_ffi_resolve_public_account`. +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_free_account_identity( + account_identity: *mut FfiAccountIdentity, +) { + if account_identity.is_null() { + return; + } + + unsafe { + let account_identity = &*account_identity; + if !account_identity.viewing_public_key.is_null() { + let slice = std::slice::from_raw_parts_mut( + account_identity.viewing_public_key.cast_mut(), + account_identity.viewing_public_key_len, + ); + drop(Box::from_raw(std::ptr::from_mut::<[u8]>(slice))); + } + } +} diff --git a/wallet-ffi/src/error.rs b/wallet-ffi/src/error.rs index 17b73075..35b084a9 100644 --- a/wallet-ffi/src/error.rs +++ b/wallet-ffi/src/error.rs @@ -41,6 +41,8 @@ pub enum WalletFfiError { InvalidTypeConversion = 15, /// Invalid Key value. InvalidKeyValue = 16, + /// Invalid program bytecode + InvalidBytecode = 17, /// Internal error (catch-all). InternalError = 99, } diff --git a/wallet-ffi/src/generic_transaction.rs b/wallet-ffi/src/generic_transaction.rs new file mode 100644 index 00000000..1195c131 --- /dev/null +++ b/wallet-ffi/src/generic_transaction.rs @@ -0,0 +1,247 @@ +use std::{collections::HashMap, ffi::CString}; + +use nssa::{privacy_preserving_transaction::circuit::ProgramWithDependencies, program::Program}; + +use crate::{ + block_on, + error::{print_error, WalletFfiError}, + map_execution_error, + wallet::get_wallet, + FfiAccountIdentity, FfiTransferResult, WalletHandle, +}; + +#[repr(C)] +pub struct SerializationHelperResult { + pub instruction_words: *mut u32, + pub instruction_words_size: usize, + pub error: WalletFfiError, +} + +impl SerializationHelperResult { + fn from_err(error: WalletFfiError) -> Self { + Self { + instruction_words: std::ptr::null_mut(), + instruction_words_size: 0, + error, + } + } +} + +/// Serialize sequence of bytes into RISC0 readable words +/// +/// # Parameters +/// - `input_instruction_data`: Valid pointer to a sequence of bytes +/// - `input_instruction_data_size`: Size of `input_instruction_data` +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `input_instruction_data` must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_serialization_helper( + input_instruction_data: *const u8, + input_instruction_data_size: usize, +) -> SerializationHelperResult { + if input_instruction_data.is_null() { + print_error("Null input pointer for instruction_data"); + return SerializationHelperResult::from_err(WalletFfiError::NullPointer); + } + + let input_slice = + unsafe { std::slice::from_raw_parts(input_instruction_data, input_instruction_data_size) }; + let res_vec_u32 = match risc0_zkvm::serde::to_vec(input_slice).map_err(|err| { + print_error(format!( + "Failed to serialize input into words with err {err}" + )); + WalletFfiError::SerializationError + }) { + Ok(res) => res, + Err(err) => return SerializationHelperResult::from_err(err), + }; + let res_len = res_vec_u32.len(); + let res_boxed = res_vec_u32.into_boxed_slice(); + let res_ptr = Box::into_raw(res_boxed).cast::(); + + SerializationHelperResult { + instruction_words: res_ptr, + instruction_words_size: res_len, + error: WalletFfiError::Success, + } +} + +#[repr(C)] +/// Intended to be created manually +pub struct FfiProgram { + pub elf_data: *const u8, + pub elf_size: usize, +} + +impl TryFrom<&FfiProgram> for Program { + type Error = WalletFfiError; + + fn try_from(value: &FfiProgram) -> Result { + let mut elf = Vec::with_capacity(value.elf_size); + + // Alignment will be different, we need to read elements one-by-one + for i in 0..value.elf_size { + elf.push(unsafe { *value.elf_data.add(i) }); + } + + Self::new(elf).map_err(|err| { + print_error(format!("Invalid program bytecode, err: {err}")); + WalletFfiError::InvalidBytecode + }) + } +} + +#[repr(C)] +/// Intended to be created manually +pub struct FfiProgramWithDependencies { + pub program: FfiProgram, + pub deps: *const FfiProgram, + pub deps_size: usize, +} + +impl TryFrom for ProgramWithDependencies { + type Error = WalletFfiError; + + fn try_from(value: FfiProgramWithDependencies) -> Result { + let mut program_map = HashMap::new(); + + let orig_program = (&value.program).try_into()?; + + // Alignment will be different, we need to read elements one-by-one + for i in 0..value.deps_size { + let program_dep: Program = unsafe { value.deps.add(i).as_ref() } + .ok_or(WalletFfiError::NullPointer)? + .try_into()?; + + program_map.insert(program_dep.id(), program_dep); + } + + Ok(Self { + program: orig_program, + dependencies: program_map, + }) + } +} + +#[repr(C)] +pub enum FfiExecutionFlow { + Public = 0, + PrivacyPreserving = 1, +} + +/// Send generic transaction +/// +/// # Parameters +/// - `handle`: Valid pointer to wallet handle +/// - `account_identities`: Valid pointer to list of `FfiAccountIdentity` +/// - `instruction_words`: Valid pointer to instruction words +/// - `out_result`: Valid pointer to `FfiTransferResult` +/// +/// # Returns +/// - `Success` on successful creation +/// - Error code on failure +/// +/// # Safety +/// - `handle` must be a valid pointer +/// - `account_identities` must be a valid pointer +/// - `instruction_words` must be a valid pointer +/// - `out_result` must be a valid pointer +#[no_mangle] +pub unsafe extern "C" fn wallet_ffi_send_generic_transaction( + handle: *mut WalletHandle, + account_identities: *const FfiAccountIdentity, + account_identities_size: usize, + instruction_words: *const u32, + instruction_words_size: usize, + program_with_dependencies: FfiProgramWithDependencies, + out_result: *mut FfiTransferResult, +) -> WalletFfiError { + let wrapper = match get_wallet(handle) { + Ok(w) => w, + Err(e) => return e, + }; + + if account_identities.is_null() { + print_error("Null output pointer for account identities list"); + return WalletFfiError::NullPointer; + } + + if instruction_words.is_null() { + print_error("Null output pointer for instruction data"); + return WalletFfiError::NullPointer; + } + + if out_result.is_null() { + print_error("Null output pointer return hash"); + return WalletFfiError::NullPointer; + } + + let wallet = match wrapper.core.lock() { + Ok(w) => w, + Err(e) => { + print_error(format!("Failed to lock wallet: {e}")); + return WalletFfiError::InternalError; + } + }; + + let mut accounts = Vec::with_capacity(account_identities_size); + let mut instruction_data = Vec::with_capacity(instruction_words_size); + + // Alignment will be different, we need to read elements one-by-one + for i in 0..account_identities_size { + accounts.push( + match match unsafe { account_identities.add(i).as_ref() } + .ok_or(WalletFfiError::NullPointer) + { + Ok(v) => v, + Err(err) => { + print_error(format!( + "account_identities_size does not match actual size of account_identities" + )); + return err; + } + } + .try_into() + { + Ok(v) => v, + Err(err) => return err, + }, + ); + } + + // Alignment will be different, we need to read elements one-by-one + for i in 0..instruction_words_size { + instruction_data.push(unsafe { *instruction_words.add(i) }); + } + + let program = match program_with_dependencies.try_into() { + Ok(v) => v, + Err(err) => return err, + }; + + match block_on(wallet.send_pub_tx(accounts, instruction_data, &program)) { + Ok(tx_hash) => { + let tx_hash = CString::new(tx_hash.to_string()) + .map_or(std::ptr::null_mut(), std::ffi::CString::into_raw); + + unsafe { + (*out_result).tx_hash = tx_hash; + (*out_result).success = true; + } + WalletFfiError::Success + } + Err(e) => { + print_error(format!("Public send failed: {e:?}")); + unsafe { + (*out_result).tx_hash = std::ptr::null_mut(); + (*out_result).success = false; + } + map_execution_error(e) + } + } +} diff --git a/wallet-ffi/src/lib.rs b/wallet-ffi/src/lib.rs index 16943d3e..e28a0560 100644 --- a/wallet-ffi/src/lib.rs +++ b/wallet-ffi/src/lib.rs @@ -42,6 +42,7 @@ use crate::error::print_error; pub mod account; pub mod error; +pub mod generic_transaction; pub mod keys; pub mod pinata; pub mod sync; diff --git a/wallet-ffi/wallet_ffi.h b/wallet-ffi/wallet_ffi.h index 211d6513..86b50de9 100644 --- a/wallet-ffi/wallet_ffi.h +++ b/wallet-ffi/wallet_ffi.h @@ -103,6 +103,10 @@ typedef enum WalletFfiError { * Invalid Key value. */ INVALID_KEY_VALUE = 16, + /** + * Invalid program bytecode + */ + INVALID_BYTECODE = 17, /** * Internal error (catch-all). */ @@ -214,13 +218,6 @@ typedef struct FfiAccount { struct FfiU128 nonce; } FfiAccount; -/** - * Public key info for a public account. - */ -typedef struct FfiPublicAccountKey { - struct FfiBytes32 public_key; -} FfiPublicAccountKey; - /** * Struct representing of account identity, given to `AccountManager` at intialization */ @@ -234,6 +231,29 @@ typedef struct FfiAccountIdentity { struct FfiU128 identifier; } FfiAccountIdentity; +typedef struct SerializationHelperResult { + uint32_t *instruction_words; + uintptr_t instruction_words_size; + enum WalletFfiError error; +} SerializationHelperResult; + +/** + * Intended to be created manually + */ +typedef struct FfiProgram { + const uint8_t *elf_data; + uintptr_t elf_size; +} FfiProgram; + +/** + * Intended to be created manually + */ +typedef struct FfiProgramWithDependencies { + struct FfiProgram program; + const struct FfiProgram *deps; + uintptr_t deps_size; +} FfiProgramWithDependencies; + /** * Result of a transfer operation. */ @@ -248,6 +268,13 @@ typedef struct FfiTransferResult { bool success; } FfiTransferResult; +/** + * Public key info for a public account. + */ +typedef struct FfiPublicAccountKey { + struct FfiBytes32 public_key; +} FfiPublicAccountKey; + /** * Create a new public account. * @@ -481,6 +508,60 @@ enum WalletFfiError wallet_ffi_import_private_account(struct WalletHandle *handl const struct FfiU128 *identifier, const char *account_state_json); +/** + * Free account identity returned by `wallet_ffi_resolve_private_account` or + * `wallet_ffi_resolve_public_account`. + * + * # Safety + * The account must be either null or a valid account returned by + * `wallet_ffi_resolve_private_account` or `wallet_ffi_resolve_public_account`. + */ +void wallet_ffi_free_account_identity(struct FfiAccountIdentity *account_identity); + +/** + * Serialize sequence of bytes into RISC0 readable words + * + * # Parameters + * - `input_instruction_data`: Valid pointer to a sequence of bytes + * - `input_instruction_data_size`: Size of `input_instruction_data` + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `input_instruction_data` must be a valid pointer + */ +struct SerializationHelperResult wallet_ffi_serialization_helper(const uint8_t *input_instruction_data, + uintptr_t input_instruction_data_size); + +/** + * Send generic transaction + * + * # Parameters + * - `handle`: Valid pointer to wallet handle + * - `account_identities`: Valid pointer to list of `FfiAccountIdentity` + * - `instruction_words`: Valid pointer to instruction words + * - `out_result`: Valid pointer to `FfiTransferResult` + * + * # Returns + * - `Success` on successful creation + * - Error code on failure + * + * # Safety + * - `handle` must be a valid pointer + * - `account_identities` must be a valid pointer + * - `instruction_words` must be a valid pointer + * - `out_result` must be a valid pointer + */ +enum WalletFfiError wallet_ffi_send_generic_transaction(struct WalletHandle *handle, + const struct FfiAccountIdentity *account_identities, + uintptr_t account_identities_size, + const uint32_t *instruction_words, + uintptr_t instruction_words_size, + struct FfiProgramWithDependencies program_with_dependencies, + struct FfiTransferResult *out_result); + /** * Get the public key for a public account. *