From dac4f1c1c62950725e1534b49569fd5085ef7351 Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 00:08:37 +0200 Subject: [PATCH 01/29] feat: refactor core into simplified WASM module --- .github/workflows/dusk_ci.yml | 4 +- Cargo.toml | 33 +- Makefile | 23 + asyncify.sh | 19 - src/ffi.rs | 838 +++++++------------------ src/imp.rs | 1082 --------------------------------- src/key.rs | 40 ++ src/lib.rs | 352 +++-------- src/tx.rs | 697 +++++++-------------- src/utils.rs | 145 +++++ tests/mock.rs | 350 ----------- tests/wallet.rs | 328 ++++++++-- 12 files changed, 1019 insertions(+), 2892 deletions(-) create mode 100644 Makefile delete mode 100755 asyncify.sh delete mode 100644 src/imp.rs create mode 100644 src/key.rs create mode 100644 src/utils.rs delete mode 100644 tests/mock.rs diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 82853d5..a4fb766 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -92,7 +92,7 @@ jobs: # Move the compiled package to the root for better paths in the npm module. # We also automatically populate the version with the given tag. run: > - ./asyncify.sh && + make package && sed -i "/\"version\": \"0.0.1\"/s/\"0.0.1\"/\"${GITHUB_REF:11}\"/" package.json && npm publish env: @@ -124,6 +124,8 @@ jobs: command: check args: --all-targets + - run: make wasm + - name: Test project if: ${{ matrix.os != 'ubuntu-latest' || matrix.toolchain != 'nightly' }} uses: actions-rs/cargo@v1 diff --git a/Cargo.toml b/Cargo.toml index 48cedd9..13c25a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,24 +5,31 @@ edition = "2021" description = "The core functionality of the Dusk wallet" license = "MPL-2.0" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] -rand_core = "^0.6" -rand_chacha = { version = "^0.3", default-features = false } -sha2 = { version = "^0.10", default-features = false } -phoenix-core = { version = "0.20.0-rc.0", default-features = false, features = ["alloc", "rkyv-impl"] } -dusk-pki = { version = "0.12", default-features = false } -dusk-bytes = "^0.1" -dusk-schnorr = { version = "0.13", default-features = false } +bytecheck = { version = "0.6", default-features = false } +dusk-bls12_381-sign = { version = "0.4", default-features = false } dusk-jubjub = { version = "0.12", default-features = false } +dusk-merkle = { version = "0.5", features = ["rkyv-impl"] } +dusk-pki = { version = "0.12", default-features = false, features = ["rkyv-impl"] } dusk-poseidon = { version = "0.30", default-features = false } +dusk-schnorr = { version = "0.13", default-features = false } +phoenix-core = { version = "0.20.0-rc.0", default-features = false, features = ["alloc", "rkyv-impl"] } poseidon-merkle = { version = "0.2.1-rc.0", features = ["rkyv-impl"] } -dusk-plonk = { version = "0.14", default-features = false } +rand_core = "^0.6" +rand_chacha = { version = "^0.3", default-features = false } +rkyv = { version = "^0.7", default-features = false } +rusk-abi = { version = "0.10.0-piecrust.0.6", default-features = false } +sha2 = { version = "^0.10", default-features = false } + +[target.'cfg(target_family = "wasm")'.dependencies] +rusk-abi = "0.10.0-piecrust.0.6" + +[target.'cfg(not(target_family = "wasm"))'.dependencies] rusk-abi = { version = "0.10.0-piecrust.0.6", default-features = false } -dusk-bls12_381-sign = { version = "0.4", default-features = false } -rkyv = { version = "0.7", default-features = false } [dev-dependencies] rand = "^0.8" - -[lib] -crate-type = ["cdylib", "rlib"] +wasmer = "=3.1" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3bab60b --- /dev/null +++ b/Makefile @@ -0,0 +1,23 @@ +help: ## Display this help screen + @grep -h \ + -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +test: wasm ## Run the wasmer tests + @cargo test + +wasm: ## Build the WASM files + @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ + cargo build \ + --release \ + --color=always \ + -Z build-std=core,alloc,panic_abort \ + -Z build-std-features=panic_immediate_abort \ + --target wasm32-unknown-unknown + +package: ## Prepare the WASM npm package + wasm-opt -O4 \ + --output-target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ + -o mod.wasm + +.PHONY: test wasm help diff --git a/asyncify.sh b/asyncify.sh deleted file mode 100755 index 75a821f..0000000 --- a/asyncify.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -arg_list=( - asyncify-import@env.compute_proof_and_propagate - asyncify-import@env.request_stct_proof - asyncify-import@env.request_wfct_proof - asyncify-import@env.fetch_anchor - asyncify-import@env.fetch_stake - asyncify-import@env.fetch_notes - asyncify-import@env.fetch_existing_nullifiers - asyncify-import@env.fetch_opening -) - -printf -v args '%s,' "${arg_list[@]}" - -wasm-opt --asyncify -O4 \ - --pass-arg "$args" \ - target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ - -o mod.wasm diff --git a/src/ffi.rs b/src/ffi.rs index 9eeaf75..c756bc3 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -4,686 +4,278 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -//! The foreign function interface for the wallet. - -use alloc::string::String; -use alloc::vec::Vec; +//! FFI bindings exposed to WASM module. +use alloc::{vec, vec::Vec}; use core::mem; -use core::num::NonZeroU32; -use core::ptr; - -use dusk_bls12_381_sign::PublicKey; -use dusk_bytes::Write; -use dusk_bytes::{DeserializableSlice, Serializable}; -use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubScalar}; -use dusk_pki::{PublicSpendKey, ViewKey}; -use dusk_plonk::prelude::Proof; -use dusk_schnorr::Signature; -use phoenix_core::{Crossover, Fee, Note}; -use poseidon_merkle::Opening as PoseidonOpening; -use rand_core::{ - impls::{next_u32_via_fill, next_u64_via_fill}, - CryptoRng, RngCore, -}; -use rusk_abi::ContractId; -use crate::tx::UnprovenTransaction; +use dusk_pki::{PublicSpendKey, SecretSpendKey}; +use phoenix_core::note::{Note, NoteType}; + use crate::{ - BalanceInfo, EnrichedNote, Error, ProverClient, StakeInfo, StateClient, - Store, Transaction, Wallet, POSEIDON_TREE_DEPTH, + key, tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, + MAX_KEY, MAX_LEN, }; -extern "C" { - /// Retrieves the seed from the store. - fn get_seed(seed: *mut [u8; 64]) -> u8; - - /// Fills a buffer with random numbers. - fn fill_random(buf: *mut u8, buf_len: u32) -> u8; - - /// Asks the node to finds the notes for a specific view key. - /// - /// An implementor should allocate - see [`malloc`] - a buffer large enough - /// to contain the serialized notes (and the corresponding block height) and - /// write them all in sequence. A pointer to the first element of the - /// buffer should then be written in `notes`, while the number of bytes - /// written should be put in `notes_len`. - /// - /// E.g: note1, block_height, note2, block_height, etc... - fn fetch_notes( - vk: *const [u8; ViewKey::SIZE], - notes: *mut *mut u8, - notes_len: *mut u32, - ) -> u8; - - /// Queries the node to find the opening for a specific note. - fn fetch_opening( - note: *const [u8; Note::SIZE], - opening: *mut u8, - opening_len: *mut u32, - ) -> u8; - - /// Asks the node to find the nullifiers that are already in the state and - /// returns them. - /// - /// The nullifiers are to be serialized in sequence and written to - /// `existing_nullifiers` and their number should be written to - /// `existing_nullifiers_len`. - fn fetch_existing_nullifiers( - nullifiers: *const u8, - nullifiers_len: u32, - existing_nullifiers: *mut u8, - existing_nullifiers_len: *mut u32, - ) -> u8; - - /// Fetches the current anchor. - fn fetch_anchor(anchor: *mut [u8; BlsScalar::SIZE]) -> u8; - - /// Fetches the current stake for a key. - /// - /// The value, eligibility, reward and counter should be written in - /// sequence, little endian, to the given buffer. If there is no value and - /// eligibility, the first 16 bytes should be zero. - fn fetch_stake( - pk: *const [u8; PublicKey::SIZE], - stake: *mut [u8; StakeInfo::SIZE], - ) -> u8; - - /// Request the node to prove the given unproven transaction. - fn compute_proof_and_propagate( - utx: *const u8, - utx_len: u32, - tx: *mut u8, - tx_len: *mut u32, - ) -> u8; - - /// Requests the node to prove STCT. - fn request_stct_proof( - inputs: *const [u8; STCT_INPUT_SIZE], - proof: *mut [u8; Proof::SIZE], - ) -> u8; - - /// Request the node to prove WFCT. - fn request_wfct_proof( - inputs: *const [u8; WFCT_INPUT_SIZE], - proof: *mut [u8; Proof::SIZE], - ) -> u8; -} - -macro_rules! unwrap_or_bail { - ($e: expr) => { - match $e { - Ok(v) => v, - Err(e) => { - return Error::::from(e).into(); - } - } - }; -} - -type FfiWallet = Wallet; -const WALLET: FfiWallet = - Wallet::new(FfiStore, FfiStateClient, FfiProverClient); - -/// Allocates memory with a given size. -#[no_mangle] -pub unsafe extern "C" fn malloc(cap: u32) -> *mut u8 { - let mut buf = Vec::with_capacity(cap as usize); - let ptr = buf.as_mut_ptr(); - mem::forget(buf); - ptr -} - -/// Free memory pointed to by the given `ptr`, and the given `cap`acity. +/// Allocates a buffer of `len` bytes on the WASM memory. #[no_mangle] -pub unsafe extern "C" fn free(ptr: *mut u8, cap: u32) { - Vec::from_raw_parts(ptr, 0, cap as usize); +pub fn malloc(len: i32) -> i32 { + let bytes = vec![0u8; len as usize]; + let ptr = bytes.as_ptr(); + mem::forget(bytes); + ptr as i32 } -/// Get the public spend key with the given index. +/// Frees a previously allocated buffer on the WASM memory. #[no_mangle] -pub unsafe extern "C" fn public_spend_key( - index: *const u64, - psk: *mut [u8; PublicSpendKey::SIZE], -) -> u8 { - let key = unwrap_or_bail!(WALLET.public_spend_key(*index)).to_bytes(); - ptr::copy_nonoverlapping(&key[0], &mut (*psk)[0], key.len()); - 0 -} - -/// Execute a generic contract call -#[no_mangle] -pub unsafe extern "C" fn execute( - contract_id: *const [u8; 32], - call_name_ptr: *mut u8, - call_name_len: *const u32, - call_data_ptr: *mut u8, - call_data_len: *const u32, - sender_index: *const u64, - refund: *const [u8; PublicSpendKey::SIZE], - gas_limit: *const u64, - gas_price: *const u64, -) -> u8 { - let contract_id = ContractId::from_bytes(*contract_id); - - // SAFETY: these buffers are expected to have been allocated with the - // correct size. If this is not the case problems with the allocator - // *may* happen. - let call_name = Vec::from_raw_parts( - call_name_ptr, - call_name_len as usize, - call_name_len as usize, - ); - let call_name = unwrap_or_bail!(String::from_utf8(call_name)); - - let call_data = Vec::from_raw_parts( - call_data_ptr, - call_data_len as usize, - call_data_len as usize, - ); - - let refund = unwrap_or_bail!(PublicSpendKey::from_bytes(&*refund)); - - unwrap_or_bail!(WALLET.execute( - &mut FfiRng, - contract_id, - call_name, - call_data, - *sender_index, - &refund, - *gas_price, - *gas_limit - )); - - 0 -} - -/// Creates a transfer transaction. -#[no_mangle] -pub unsafe extern "C" fn transfer( - sender_index: *const u64, - refund: *const [u8; PublicSpendKey::SIZE], - receiver: *const [u8; PublicSpendKey::SIZE], - value: *const u64, - gas_limit: *const u64, - gas_price: *const u64, - ref_id: Option<&u64>, -) -> u8 { - let refund = unwrap_or_bail!(PublicSpendKey::from_bytes(&*refund)); - let receiver = unwrap_or_bail!(PublicSpendKey::from_bytes(&*receiver)); - - let ref_id = - BlsScalar::from(ref_id.copied().unwrap_or_else(|| FfiRng.next_u64())); - - unwrap_or_bail!(WALLET.transfer( - &mut FfiRng, - *sender_index, - &refund, - &receiver, - *value, - *gas_price, - *gas_limit, - ref_id - )); - - 0 -} - -/// Creates a stake transaction. -#[no_mangle] -pub unsafe extern "C" fn stake( - sender_index: *const u64, - staker_index: *const u64, - refund: *const [u8; PublicSpendKey::SIZE], - value: *const u64, - gas_limit: *const u64, - gas_price: *const u64, -) -> u8 { - let refund = unwrap_or_bail!(PublicSpendKey::from_bytes(&*refund)); - - unwrap_or_bail!(WALLET.stake( - &mut FfiRng, - *sender_index, - *staker_index, - &refund, - *value, - *gas_price, - *gas_limit - )); - - 0 -} - -/// Unstake the value previously staked using the [`stake`] function. -#[no_mangle] -pub unsafe extern "C" fn unstake( - sender_index: *const u64, - staker_index: *const u64, - refund: *const [u8; PublicSpendKey::SIZE], - gas_limit: *const u64, - gas_price: *const u64, -) -> u8 { - let refund = unwrap_or_bail!(PublicSpendKey::from_bytes(&*refund)); - - unwrap_or_bail!(WALLET.unstake( - &mut FfiRng, - *sender_index, - *staker_index, - &refund, - *gas_price, - *gas_limit - )); - - 0 -} - -/// Withdraw the rewards accumulated as a result of staking and taking part in -/// the consensus. -#[no_mangle] -pub unsafe extern "C" fn withdraw( - sender_index: *const u64, - staker_index: *const u64, - refund: *const [u8; PublicSpendKey::SIZE], - gas_limit: *const u64, - gas_price: *const u64, -) -> u8 { - let refund = unwrap_or_bail!(PublicSpendKey::from_bytes(&*refund)); - - unwrap_or_bail!(WALLET.withdraw( - &mut FfiRng, - *sender_index, - *staker_index, - &refund, - *gas_price, - *gas_limit - )); - - 0 -} - -/// Gets the balance of a secret spend key. -#[no_mangle] -pub unsafe extern "C" fn get_balance( - ssk_index: *const u64, - balance: *mut [u8; BalanceInfo::SIZE], -) -> u8 { - let b = unwrap_or_bail!(WALLET.get_balance(*ssk_index)).to_bytes(); - ptr::copy_nonoverlapping(&b[0], &mut (*balance)[0], b.len()); - 0 -} - -/// Gets the stake of a key. The value, eligibility, reward, and counter are -/// written in sequence to the given buffer. If there is no value and -/// eligibility the first 16 bytes will be zero. -#[no_mangle] -pub unsafe extern "C" fn get_stake( - sk_index: *const u64, - stake: *mut [u8; StakeInfo::SIZE], -) -> u8 { - let s = unwrap_or_bail!(WALLET.get_stake(*sk_index)).to_bytes(); - ptr::copy_nonoverlapping(&s[0], &mut (*stake)[0], s.len()); - 0 -} - -struct FfiStore; - -impl Store for FfiStore { - type Error = u8; - - fn get_seed(&self) -> Result<[u8; 64], Self::Error> { - let mut seed = [0; 64]; - unsafe { - let r = get_seed(&mut seed); - if r != 0 { - return Err(r); - } - } - Ok(seed) +pub fn free_mem(ptr: i32, len: i32) { + let ptr = ptr as *mut u8; + let len = len as usize; + unsafe { + Vec::from_raw_parts(ptr, len, len); } } -const STCT_INPUT_SIZE: usize = Fee::SIZE - + Crossover::SIZE - + u64::SIZE - + JubJubScalar::SIZE - + BlsScalar::SIZE - + Signature::SIZE; - -const WFCT_INPUT_SIZE: usize = - JubJubAffine::SIZE + u64::SIZE + JubJubScalar::SIZE; - -struct FfiStateClient; - -impl StateClient for FfiStateClient { - type Error = u8; +/// Computes the total balance of the given notes. +/// +/// The arguments are expected to be rkyv serialized [BalanceArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn balance(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + + let BalanceArgs { seed, notes } = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return BalanceResponse::fail(), + }; - fn fetch_notes( - &self, - vk: &ViewKey, - ) -> Result, Self::Error> { - let mut notes_ptr = ptr::null_mut(); - let mut notes_len = 0; + let notes: Vec = match rkyv::from_bytes(¬es) { + Ok(n) => n, + Err(_) => return BalanceResponse::fail(), + }; - let notes_buf = unsafe { - let r = fetch_notes(&vk.to_bytes(), &mut notes_ptr, &mut notes_len); - if r != 0 { - return Err(r); + let mut keys = unsafe { [mem::zeroed(); MAX_KEY] }; + let mut keys_len = 0; + let mut sum = 0u64; + + 'outer: for note in notes { + // we iterate all the available keys until one can successfully decrypt + // the note. if all fails, returns false + for idx in 0..MAX_KEY { + if keys_len == idx { + keys[idx] = key::derive_vk(&seed, idx as u64); + keys_len += 1; } - // SAFETY: the buffer is expected to have been allocated with the - // correct size. If this is not the case problems with the allocator - // *may* happen. - Vec::from_raw_parts( - notes_ptr, - notes_len as usize, - notes_len as usize, - ) - }; - - let num_notes = notes_len as usize / (Note::SIZE + u64::SIZE); - let mut notes = Vec::with_capacity(num_notes); - - let mut buf = ¬es_buf[..]; - for _ in 0..num_notes { - let note = Note::from_reader(&mut buf).map_err( - Error::::from, - )?; - let block_height = u64::from_reader(&mut buf).map_err( - Error::::from, - )?; - notes.push((note, block_height)); - } - - Ok(notes) - } - - fn fetch_anchor(&self) -> Result { - let mut scalar_buf = [0; BlsScalar::SIZE]; - unsafe { - let r = fetch_anchor(&mut scalar_buf); - if r != 0 { - return Err(r); + if let Ok(v) = note.value(Some(&keys[idx])) { + sum = sum.saturating_add(v); + continue 'outer; } } - let scalar = BlsScalar::from_bytes(&scalar_buf).map_err( - Error::::from, - )?; - Ok(scalar) + return BalanceResponse::fail(); } - fn fetch_existing_nullifiers( - &self, - nullifiers: &[BlsScalar], - ) -> Result, Self::Error> { - let nullifiers_len = nullifiers.len(); - let mut nullifiers_buf = vec![0u8; BlsScalar::SIZE * nullifiers_len]; - - // If no nullifiers come in, then none of them exist in the state. - if nullifiers_len == 0 { - return Ok(vec![]); - } + BalanceResponse::success(sum) +} - let mut writer = &mut nullifiers_buf[..]; +/// Computes a serialized unproven transaction from the given arguments. +/// +/// The arguments are expected to be rkyv serialized [ExecuteArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn execute(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + let args = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return ExecuteResponse::fail(), + }; - for nullifier in nullifiers { - writer.write(&nullifier.to_bytes()).map_err( - Error::::from, - )?; - } + fn inner( + ExecuteArgs { + seed, + rng_seed, + inputs, + openings, + refund, + output, + crossover, + gas_limit, + gas_price, + call, + }: ExecuteArgs, + ) -> Option<(Vec, Vec)> { + let inputs: Vec = rkyv::from_bytes(&inputs).ok()?; + let openings: Vec = rkyv::from_bytes(&openings).ok()?; + let refund: PublicSpendKey = rkyv::from_bytes(&refund).ok()?; + let output: Option = rkyv::from_bytes(&output).ok()?; + let call: Option = rkyv::from_bytes(&call).ok()?; + + let value = output.as_ref().map(|o| o.value).unwrap_or(0); + let total_output = + gas_limit.saturating_mul(gas_price).saturating_add(value); + + let mut keys = unsafe { [mem::zeroed(); MAX_KEY] }; + let mut keys_ssk = + unsafe { [mem::zeroed::(); MAX_KEY] }; + let mut keys_len = 0; + let mut openings = openings.into_iter(); + let mut full_inputs = Vec::with_capacity(inputs.len()); + + 'outer: for input in inputs { + // we iterate all the available keys until one can successfully + // decrypt the note. if any fails, returns false + for idx in 0..MAX_KEY { + if keys_len == idx { + keys_ssk[idx] = key::derive_ssk(&seed, idx as u64); + keys[idx] = keys_ssk[idx].view_key(); + keys_len += 1; + } - let mut existing_nullifiers_buf = - vec![0u8; BlsScalar::SIZE * nullifiers_len]; - let mut existing_nullifiers_len = 0; - - unsafe { - let r = fetch_existing_nullifiers( - &nullifiers_buf[0], - nullifiers_len as u32, - &mut existing_nullifiers_buf[0], - &mut existing_nullifiers_len, - ); - if r != 0 { - return Err(r); + if let Ok(value) = input.value(Some(&keys[idx])) { + let opening = openings.next()?; + full_inputs.push((input, opening, value, idx)); + continue 'outer; + } } - }; - - let mut existing_nullifiers = - Vec::with_capacity(existing_nullifiers_len as usize); - let mut reader = &existing_nullifiers_buf[..]; - for _ in 0..existing_nullifiers_len { - existing_nullifiers.push( - BlsScalar::from_reader(&mut reader).map_err( - Error::::from, - )?, - ); + return None; } - Ok(existing_nullifiers) - } - - fn fetch_opening( - &self, - note: &Note, - ) -> Result, Self::Error> { - const OPENING_BUF_SIZE: usize = 3000; - - let mut opening_buf = Vec::with_capacity(OPENING_BUF_SIZE); - let mut opening_len = 0; - - let note = note.to_bytes(); - unsafe { - let r = fetch_opening( - ¬e, - opening_buf.as_mut_ptr(), - &mut opening_len, - ); - if r != 0 { - return Err(r); - } + // optimizes the inputs given the total amount + let (unspent, inputs) = utils::knapsack(full_inputs, total_output)?; + let inputs: Vec<_> = inputs + .into_iter() + .map(|(note, opening, value, idx)| tx::PreInput { + note, + opening, + value, + ssk: &keys_ssk[idx], + }) + .collect(); + let total_input: u64 = inputs.iter().map(|i| i.value).sum(); + let total_refund = total_input.saturating_sub(total_output); + + let mut outputs: Vec = Vec::with_capacity(2); + if let Some(o) = output { + outputs.push(o); } + if total_refund > 0 { + outputs.push(tx::OutputValue { + r#type: NoteType::Obfuscated, + value: total_refund, + receiver: refund, + ref_id: 0, + }); + } + + let rng = &mut utils::rng(&rng_seed); + let tx = tx::UnprovenTransaction::new( + rng, inputs, outputs, &refund, gas_limit, gas_price, crossover, + call, + )?; - let branch = rkyv::from_bytes(&opening_buf[..opening_len as usize]) - .map_err( - Error::::from, - )?; + let unspent = rkyv::to_bytes::<_, MAX_LEN>(&unspent).ok()?.into_vec(); + let tx = rkyv::to_bytes::<_, MAX_LEN>(&tx).ok()?.into_vec(); - Ok(branch) + Some((unspent, tx)) } - fn fetch_stake(&self, pk: &PublicKey) -> Result { - let pk = pk.to_bytes(); - let mut stake_buf = [0u8; StakeInfo::SIZE]; + let (unspent, tx) = match inner(args) { + Some(t) => t, + None => return ExecuteResponse::fail(), + }; - unsafe { - let r = fetch_stake(&pk, &mut stake_buf); - if r != 0 { - return Err(r); - } - } + let unspent_ptr = unspent.as_ptr() as u64; + let unspent_len = unspent.len() as u64; + let tx_ptr = tx.as_ptr() as u64; + let tx_len = tx.len() as u64; - let stake = StakeInfo::from_bytes(&stake_buf).map_err( - Error::::from, - )?; + mem::forget(unspent); + mem::forget(tx); - Ok(stake) - } + ExecuteResponse::success(unspent_ptr, unspent_len, tx_ptr, tx_len) } -struct FfiProverClient; - -impl ProverClient for FfiProverClient { - type Error = u8; - - fn compute_proof_and_propagate( - &self, - utx: &UnprovenTransaction, - ) -> Result { - let utx_bytes = utx.to_var_bytes(); - - // A transaction is always smaller than an unproven transaction - let mut tx_buf = vec![0; utx_bytes.len()]; - let mut tx_len = 0; - - unsafe { - let r = compute_proof_and_propagate( - &utx_bytes[0], - utx_bytes.len() as u32, - &mut tx_buf[0], - &mut tx_len, - ); - if r != 0 { - return Err(r); - } - } +impl BalanceResponse { + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; - let transaction = Transaction::from_slice(&tx_buf[..tx_len as usize]) - .map_err( - Error::::from, - )?; + let ptr = b.as_ptr() as i32; + mem::forget(b); - Ok(transaction) + ptr } - fn request_stct_proof( - &self, - fee: &Fee, - crossover: &Crossover, - value: u64, - blinder: JubJubScalar, - address: BlsScalar, - signature: Signature, - ) -> Result { - let mut buf = [0; STCT_INPUT_SIZE]; - - let mut writer = &mut buf[..]; - writer.write(&fee.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&crossover.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&value.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&blinder.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&address.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&signature.to_bytes()).map_err( - Error::::from, - )?; - - let mut proof_buf = [0; Proof::SIZE]; - - unsafe { - let r = request_stct_proof(&buf, &mut proof_buf); - if r != 0 { - return Err(r); - } + /// Returns a representation of a successful balance operation with the + /// computed value. + pub fn success(value: u64) -> i32 { + Self { + success: true, + value, } - - let proof = Proof::from_bytes(&proof_buf).map_err( - Error::::from, - )?; - Ok(proof) + .as_i32_ptr() } - fn request_wfct_proof( - &self, - commitment: JubJubAffine, - value: u64, - blinder: JubJubScalar, - ) -> Result { - let mut buf = [0; WFCT_INPUT_SIZE]; - - let mut writer = &mut buf[..]; - writer.write(&commitment.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&value.to_bytes()).map_err( - Error::::from, - )?; - writer.write(&blinder.to_bytes()).map_err( - Error::::from, - )?; - - let mut proof_buf = [0; Proof::SIZE]; - - unsafe { - let r = request_wfct_proof(&buf, &mut proof_buf); - if r != 0 { - return Err(r); - } + /// Returns a representation of the failure of the balance operation. + pub fn fail() -> i32 { + Self { + success: false, + value: 0, } - - let proof = Proof::from_bytes(&proof_buf).map_err( - Error::::from, - )?; - Ok(proof) + .as_i32_ptr() } } -struct FfiRng; - -impl CryptoRng for FfiRng {} - -impl RngCore for FfiRng { - fn next_u32(&mut self) -> u32 { - next_u32_via_fill(self) - } +impl ExecuteResponse { + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; - fn next_u64(&mut self) -> u64 { - next_u64_via_fill(self) - } + let ptr = b.as_ptr() as i32; + mem::forget(b); - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.try_fill_bytes(dest).ok(); + ptr } - fn try_fill_bytes( - &mut self, - dest: &mut [u8], - ) -> Result<(), rand_core::Error> { - let buf = dest.as_mut_ptr(); - let len = dest.len(); - - // SAFETY: this is unsafe since the passed function is not guaranteed to - // be a CSPRNG running in a secure context. We therefore consider it the - // responsibility of the user to pass a good generator. - unsafe { - match fill_random(buf, len as u32) { - 0 => Ok(()), - v => { - let nzu = NonZeroU32::new(v as u32).unwrap(); - Err(rand_core::Error::from(nzu)) - } - } + /// Returns a representation of a successful execute operation with the + /// underlying unspent notes list and the unproven transaction. + pub fn success( + unspent_ptr: u64, + unspent_len: u64, + tx_ptr: u64, + tx_len: u64, + ) -> i32 { + Self { + success: true, + unspent_ptr, + unspent_len, + tx_ptr, + tx_len, } + .as_i32_ptr() } -} -impl From> - for u8 -{ - fn from(e: Error) -> Self { - match e { - Error::Store(_) => 255, - Error::Rng(_) => 254, - Error::Bytes(_) => 253, - Error::State(_) => 252, - Error::Prover(_) => 251, - Error::NotEnoughBalance => 250, - Error::NoteCombinationProblem => 249, - Error::Rkyv => 248, - Error::Phoenix(_) => 247, - Error::AlreadyStaked { .. } => 246, - Error::NotStaked { .. } => 245, - Error::NoReward { .. } => 244, - Error::Utf8(_) => 243, + /// Returns a representation of the failure of the execute operation. + pub fn fail() -> i32 { + Self { + success: false, + unspent_ptr: 0, + unspent_len: 0, + tx_ptr: 0, + tx_len: 0, } + .as_i32_ptr() } } diff --git a/src/imp.rs b/src/imp.rs deleted file mode 100644 index 916ae4e..0000000 --- a/src/imp.rs +++ /dev/null @@ -1,1082 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) DUSK NETWORK. All rights reserved. - -use crate::tx::UnprovenTransaction; -use crate::{ - BalanceInfo, ProverClient, StakeInfo, StateClient, Store, MAX_CALL_SIZE, -}; - -use core::convert::Infallible; - -use alloc::string::{FromUtf8Error, String}; -use alloc::vec::Vec; - -use dusk_bls12_381_sign::{PublicKey, SecretKey, Signature}; -use dusk_bytes::{Error as BytesError, Serializable}; -use dusk_jubjub::{BlsScalar, JubJubScalar}; -use dusk_pki::{ - Ownable, PublicSpendKey, SecretKey as SchnorrKey, SecretSpendKey, - StealthAddress, -}; -use dusk_schnorr::Signature as SchnorrSignature; -use phoenix_core::transaction::*; -use phoenix_core::{Error as PhoenixError, Fee, Note, NoteType}; -use rand_core::{CryptoRng, Error as RngError, RngCore}; -use rkyv::ser::serializers::{ - AllocScratchError, AllocSerializer, CompositeSerializerError, - SharedSerializeMapError, -}; -use rkyv::validation::validators::CheckDeserializeError; -use rkyv::Serialize; -use rusk_abi::ContractId; - -const MAX_INPUT_NOTES: usize = 4; - -const TX_STAKE: &str = "stake"; -const TX_UNSTAKE: &str = "unstake"; -const TX_WITHDRAW: &str = "withdraw"; -const TX_ADD_ALLOWLIST: &str = "allow"; - -type SerializerError = CompositeSerializerError< - Infallible, - AllocScratchError, - SharedSerializeMapError, ->; - -/// The error type returned by this crate. -#[derive(Debug)] -#[allow(clippy::large_enum_variant)] -pub enum Error { - /// Underlying store error. - Store(S::Error), - /// Error originating from the state client. - State(SC::Error), - /// Error originating from the prover client. - Prover(PC::Error), - /// Rkyv serialization. - Rkyv, - /// Random number generator error. - Rng(RngError), - /// Serialization and deserialization of Dusk types. - Bytes(BytesError), - /// Bytes were meant to be utf8 but aren't. - Utf8(FromUtf8Error), - /// Originating from the transaction model. - Phoenix(PhoenixError), - /// Not enough balance to perform transaction. - NotEnoughBalance, - /// Note combination for the given value is impossible given the maximum - /// amount if inputs in a transaction. - NoteCombinationProblem, - /// The key is already staked. This happens when there already is an amount - /// staked for a key and the user tries to make a stake transaction. - AlreadyStaked { - /// The key that already has a stake. - key: PublicKey, - /// Information about the key's stake. - stake: StakeInfo, - }, - /// The key is not staked. This happens when a key doesn't have an amount - /// staked and the user tries to make an unstake transaction. - NotStaked { - /// The key that is not staked. - key: PublicKey, - /// Information about the key's stake. - stake: StakeInfo, - }, - /// The key has no reward. This happens when a key has no reward in the - /// stake contract and the user tries to make a withdraw transaction. - NoReward { - /// The key that has no reward. - key: PublicKey, - /// Information about the key's stake. - stake: StakeInfo, - }, -} - -impl Error { - /// Returns an error from the underlying store error. - pub fn from_store_err(se: S::Error) -> Self { - Self::Store(se) - } - /// Returns an error from the underlying state client. - pub fn from_state_err(se: SC::Error) -> Self { - Self::State(se) - } - /// Returns an error from the underlying prover client. - pub fn from_prover_err(pe: PC::Error) -> Self { - Self::Prover(pe) - } -} - -impl From - for Error -{ - fn from(_: SerializerError) -> Self { - Self::Rkyv - } -} - -impl - From> for Error -{ - fn from(_: CheckDeserializeError) -> Self { - Self::Rkyv - } -} - -impl From - for Error -{ - fn from(re: RngError) -> Self { - Self::Rng(re) - } -} - -impl From - for Error -{ - fn from(be: BytesError) -> Self { - Self::Bytes(be) - } -} - -impl From - for Error -{ - fn from(err: FromUtf8Error) -> Self { - Self::Utf8(err) - } -} - -impl From - for Error -{ - fn from(pe: PhoenixError) -> Self { - Self::Phoenix(pe) - } -} - -/// A wallet implementation. -/// -/// This is responsible for holding the keys, and performing operations like -/// creating transactions. -pub struct Wallet { - store: S, - state: SC, - prover: PC, -} - -impl Wallet { - /// Create a new wallet given the underlying store and node client. - pub const fn new(store: S, state: SC, prover: PC) -> Self { - Self { - store, - state, - prover, - } - } - - /// Return the inner Store reference - pub const fn store(&self) -> &S { - &self.store - } - - /// Return the inner State reference - pub const fn state(&self) -> &SC { - &self.state - } - - /// Return the inner Prover reference - pub const fn prover(&self) -> &PC { - &self.prover - } -} - -impl Wallet -where - S: Store, - SC: StateClient, - PC: ProverClient, -{ - /// Retrieve the public spend key with the given index. - pub fn public_spend_key( - &self, - index: u64, - ) -> Result> { - self.store - .retrieve_ssk(index) - .map(|ssk| ssk.public_spend_key()) - .map_err(Error::from_store_err) - } - - /// Retrieve the public key with the given index. - pub fn public_key( - &self, - index: u64, - ) -> Result> { - self.store - .retrieve_sk(index) - .map(|sk| From::from(&sk)) - .map_err(Error::from_store_err) - } - - /// Fetches the notes and nullifiers in the state and returns the notes that - /// are still available for spending. - fn unspent_notes( - &self, - ssk: &SecretSpendKey, - ) -> Result, Error> { - let vk = ssk.view_key(); - - let notes = - self.state.fetch_notes(&vk).map_err(Error::from_state_err)?; - - let nullifiers: Vec<_> = - notes.iter().map(|(n, _)| n.gen_nullifier(ssk)).collect(); - - let existing_nullifiers = self - .state - .fetch_existing_nullifiers(&nullifiers) - .map_err(Error::from_state_err)?; - - let unspent_notes = notes - .into_iter() - .zip(nullifiers.into_iter()) - .filter(|(_, nullifier)| !existing_nullifiers.contains(nullifier)) - .map(|((note, _), _)| note) - .collect(); - - Ok(unspent_notes) - } - - /// Here we fetch the notes and perform a "minimum number of notes - /// required" algorithm to select which ones to use for this TX. This is - /// done by picking notes largest to smallest until they combined have - /// enough accumulated value. - /// - /// We also return the outputs with a possible change note (if applicable). - #[allow(clippy::type_complexity)] - fn inputs_and_change_output( - &self, - rng: &mut Rng, - sender: &SecretSpendKey, - refund: &PublicSpendKey, - value: u64, - ) -> Result< - ( - Vec<(Note, u64, JubJubScalar)>, - Vec<(Note, u64, JubJubScalar)>, - ), - Error, - > { - let notes = self.unspent_notes(sender)?; - let mut notes_and_values = Vec::with_capacity(notes.len()); - - let sender_vk = sender.view_key(); - - let mut accumulated_value = 0; - for note in notes.into_iter() { - let val = note.value(Some(&sender_vk))?; - let blinder = note.blinding_factor(Some(&sender_vk))?; - - accumulated_value += val; - notes_and_values.push((note, val, blinder)); - } - - if accumulated_value < value { - return Err(Error::NotEnoughBalance); - } - - let inputs = pick_notes(value, notes_and_values); - - if inputs.is_empty() { - return Err(Error::NoteCombinationProblem); - } - - let change = inputs.iter().map(|v| v.1).sum::() - value; - - let mut outputs = vec![]; - if change > 0 { - let nonce = BlsScalar::random(rng); - let (change_note, change_blinder) = - generate_obfuscated_note(rng, refund, change, nonce); - - outputs.push((change_note, change, change_blinder)) - } - - Ok((inputs, outputs)) - } - - /// Execute a generic contract call - #[allow(clippy::too_many_arguments)] - pub fn execute( - &self, - rng: &mut Rng, - contract_id: ContractId, - call_name: String, - call_data: C, - sender_index: u64, - refund: &PublicSpendKey, - gas_limit: u64, - gas_price: u64, - ) -> Result> - where - Rng: RngCore + CryptoRng, - C: Serialize>, - { - let sender = self - .store - .retrieve_ssk(sender_index) - .map_err(Error::from_store_err)?; - - let (inputs, outputs) = self.inputs_and_change_output( - rng, - &sender, - refund, - gas_limit * gas_price, - )?; - - let fee = Fee::new(rng, gas_limit, gas_price, refund); - - let call_data = rkyv::to_bytes(&call_data)?.to_vec(); - let call = (contract_id, call_name, call_data); - - let utx = UnprovenTransaction::new( - rng, - &self.state, - &sender, - inputs, - outputs, - fee, - None, - Some(call), - ) - .map_err(Error::from_state_err)?; - - self.prover - .compute_proof_and_propagate(&utx) - .map_err(Error::from_prover_err) - } - - /// Transfer Dusk from one key to another. - #[allow(clippy::too_many_arguments)] - pub fn transfer( - &self, - rng: &mut Rng, - sender_index: u64, - refund: &PublicSpendKey, - receiver: &PublicSpendKey, - value: u64, - gas_limit: u64, - gas_price: u64, - ref_id: BlsScalar, - ) -> Result> { - let sender = self - .store - .retrieve_ssk(sender_index) - .map_err(Error::from_store_err)?; - - let (inputs, mut outputs) = self.inputs_and_change_output( - rng, - &sender, - refund, - value + gas_limit * gas_price, - )?; - - let (output_note, output_blinder) = - generate_obfuscated_note(rng, receiver, value, ref_id); - - outputs.push((output_note, value, output_blinder)); - - let crossover = None; - let fee = Fee::new(rng, gas_limit, gas_price, refund); - - let utx = UnprovenTransaction::new( - rng, - &self.state, - &sender, - inputs, - outputs, - fee, - crossover, - None, - ) - .map_err(Error::from_state_err)?; - - self.prover - .compute_proof_and_propagate(&utx) - .map_err(Error::from_prover_err) - } - - /// Stakes an amount of Dusk. - #[allow(clippy::too_many_arguments)] - pub fn stake( - &self, - rng: &mut Rng, - sender_index: u64, - staker_index: u64, - refund: &PublicSpendKey, - value: u64, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let sender = self - .store - .retrieve_ssk(sender_index) - .map_err(Error::from_store_err)?; - - let sk = self - .store - .retrieve_sk(staker_index) - .map_err(Error::from_store_err)?; - let pk = PublicKey::from(&sk); - - let (inputs, outputs) = self.inputs_and_change_output( - rng, - &sender, - refund, - value + gas_limit * gas_price, - )?; - - let stake = - self.state.fetch_stake(&pk).map_err(Error::from_state_err)?; - if stake.amount.is_some() { - return Err(Error::AlreadyStaked { key: pk, stake }); - } - - let blinder = JubJubScalar::random(rng); - let note = Note::obfuscated(rng, refund, value, blinder); - let (mut fee, crossover) = note - .try_into() - .expect("Obfuscated notes should always yield crossovers"); - - fee.gas_limit = gas_limit; - fee.gas_price = gas_price; - - let contract_id = rusk_abi::STAKE_CONTRACT; - let address = rusk_abi::contract_to_scalar(&contract_id); - - let contract_id = rusk_abi::contract_to_scalar(&contract_id); - - let stct_message = - stct_signature_message(&crossover, value, contract_id); - let stct_message = dusk_poseidon::sponge::hash(&stct_message); - - let sk_r = *sender.sk_r(fee.stealth_address()).as_ref(); - let secret = SchnorrKey::from(sk_r); - - let stct_signature = SchnorrSignature::new(&secret, rng, stct_message); - - let spend_proof = self - .prover - .request_stct_proof( - &fee, - &crossover, - value, - blinder, - address, - stct_signature, - ) - .map_err(Error::from_prover_err)? - .to_bytes() - .to_vec(); - - let signature = stake_sign(&sk, &pk, stake.counter, value); - - let stake = Stake { - public_key: pk, - signature, - value, - proof: spend_proof, - }; - - let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&stake)?.to_vec(); - let call = - (rusk_abi::STAKE_CONTRACT, String::from(TX_STAKE), call_data); - - let utx = UnprovenTransaction::new( - rng, - &self.state, - &sender, - inputs, - outputs, - fee, - Some((crossover, value, blinder)), - Some(call), - ) - .map_err(Error::from_state_err)?; - - self.prover - .compute_proof_and_propagate(&utx) - .map_err(Error::from_prover_err) - } - - /// Unstake a key from the stake contract. - pub fn unstake( - &self, - rng: &mut Rng, - sender_index: u64, - staker_index: u64, - refund: &PublicSpendKey, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let sender = self - .store - .retrieve_ssk(sender_index) - .map_err(Error::from_store_err)?; - - let sk = self - .store - .retrieve_sk(staker_index) - .map_err(Error::from_store_err)?; - let pk = PublicKey::from(&sk); - - let (inputs, outputs) = self.inputs_and_change_output( - rng, - &sender, - refund, - gas_limit * gas_price, - )?; - - let stake = - self.state.fetch_stake(&pk).map_err(Error::from_state_err)?; - let (value, _) = - stake.amount.ok_or(Error::NotStaked { key: pk, stake })?; - - let blinder = JubJubScalar::random(rng); - - // Since we're not transferring value *to* the contract the crossover - // shouldn't contain a value. As such the note used to create it should - // be valueless as well. - let note = Note::obfuscated(rng, refund, 0, blinder); - let (mut fee, crossover) = note - .try_into() - .expect("Obfuscated notes should always yield crossovers"); - - fee.gas_limit = gas_limit; - fee.gas_price = gas_price; - - let unstake_note = - Note::transparent(rng, &sender.public_spend_key(), value); - let unstake_blinder = unstake_note - .blinding_factor(None) - .expect("Note is transparent so blinding factor is unencrypted"); - - let unstake_proof = self - .prover - .request_wfct_proof( - unstake_note.value_commitment().into(), - value, - unstake_blinder, - ) - .map_err(Error::from_prover_err)? - .to_bytes() - .to_vec(); - - let signature = unstake_sign(&sk, &pk, stake.counter, unstake_note); - - let unstake = Unstake { - public_key: pk, - signature, - note: unstake_note, - proof: unstake_proof, - }; - - let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&unstake)?.to_vec(); - let call = ( - rusk_abi::STAKE_CONTRACT, - String::from(TX_UNSTAKE), - call_data, - ); - - let utx = UnprovenTransaction::new( - rng, - &self.state, - &sender, - inputs, - outputs, - fee, - Some((crossover, 0, blinder)), - Some(call), - ) - .map_err(Error::from_state_err)?; - - self.prover - .compute_proof_and_propagate(&utx) - .map_err(Error::from_prover_err) - } - - /// Withdraw the reward a key has reward if accumulated by staking and - /// taking part in operating the network. - pub fn withdraw( - &self, - rng: &mut Rng, - sender_index: u64, - staker_index: u64, - refund: &PublicSpendKey, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let sender = self - .store - .retrieve_ssk(sender_index) - .map_err(Error::from_store_err)?; - let sender_psk = sender.public_spend_key(); - - let sk = self - .store - .retrieve_sk(staker_index) - .map_err(Error::from_store_err)?; - let pk = PublicKey::from(&sk); - - let (inputs, outputs) = self.inputs_and_change_output( - rng, - &sender, - refund, - gas_limit * gas_price, - )?; - - let stake = - self.state.fetch_stake(&pk).map_err(Error::from_state_err)?; - if stake.reward == 0 { - return Err(Error::NoReward { key: pk, stake }); - } - - let withdraw_r = JubJubScalar::random(rng); - let address = sender_psk.gen_stealth_address(&withdraw_r); - let nonce = BlsScalar::random(rng); - - let signature = withdraw_sign(&sk, &pk, stake.counter, address, nonce); - - // Since we're not transferring value *to* the contract the crossover - // shouldn't contain a value. As such the note used to created it should - // be valueless as well. - let blinder = JubJubScalar::random(rng); - let note = Note::obfuscated(rng, refund, 0, blinder); - let (mut fee, crossover) = note - .try_into() - .expect("Obfuscated notes should always yield crossovers"); - - fee.gas_limit = gas_limit; - fee.gas_price = gas_price; - - let withdraw = Withdraw { - public_key: pk, - signature, - address, - nonce, - }; - let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&withdraw)?.to_vec(); - - let contract_id = rusk_abi::STAKE_CONTRACT; - let call = (contract_id, String::from(TX_WITHDRAW), call_data); - - let utx = UnprovenTransaction::new( - rng, - &self.state, - &sender, - inputs, - outputs, - fee, - Some((crossover, 0, blinder)), - Some(call), - ) - .map_err(Error::from_state_err)?; - - self.prover - .compute_proof_and_propagate(&utx) - .map_err(Error::from_prover_err) - } - - /// Allow a `staker` public key. - #[allow(clippy::too_many_arguments)] - pub fn allow( - &self, - rng: &mut Rng, - sender_index: u64, - owner_index: u64, - refund: &PublicSpendKey, - staker: &PublicKey, - gas_limit: u64, - gas_price: u64, - ) -> Result> { - let sender = self - .store - .retrieve_ssk(sender_index) - .map_err(Error::from_store_err)?; - - let owner_sk = self - .store - .retrieve_sk(owner_index) - .map_err(Error::from_store_err)?; - let owner_pk = PublicKey::from(&owner_sk); - - let (inputs, outputs) = self.inputs_and_change_output( - rng, - &sender, - refund, - gas_limit * gas_price, - )?; - - let stake = self - .state - .fetch_stake(&owner_pk) - .map_err(Error::from_state_err)?; - - let signature = allow_sign(&owner_sk, &owner_pk, stake.counter, staker); - - // Since we're not transferring value *to* the contract the crossover - // shouldn't contain a value. As such the note used to created it should - // be valueless as well. - let blinder = JubJubScalar::random(rng); - let note = Note::obfuscated(rng, refund, 0, blinder); - let (mut fee, crossover) = note - .try_into() - .expect("Obfuscated notes should always yield crossovers"); - - fee.gas_limit = gas_limit; - fee.gas_price = gas_price; - - let allow = Allow { - public_key: *staker, - owner: owner_pk, - signature, - }; - let call_data = rkyv::to_bytes::<_, MAX_CALL_SIZE>(&allow)?.to_vec(); - - let contract_id = rusk_abi::STAKE_CONTRACT; - let call = (contract_id, String::from(TX_ADD_ALLOWLIST), call_data); - - let utx = UnprovenTransaction::new( - rng, - &self.state, - &sender, - inputs, - outputs, - fee, - Some((crossover, 0, blinder)), - Some(call), - ) - .map_err(Error::from_state_err)?; - - self.prover - .compute_proof_and_propagate(&utx) - .map_err(Error::from_prover_err) - } - - /// Gets the balance of a key. - pub fn get_balance( - &self, - ssk_index: u64, - ) -> Result> { - let sender = self - .store - .retrieve_ssk(ssk_index) - .map_err(Error::from_store_err)?; - let vk = sender.view_key(); - - let notes = self.unspent_notes(&sender)?; - let mut values = Vec::with_capacity(notes.len()); - - for note in notes.into_iter() { - values.push(note.value(Some(&vk))?); - } - values.sort_by(|a, b| b.cmp(a)); - - let spendable = values.iter().take(MAX_INPUT_NOTES).sum(); - let value = - spendable + values.iter().skip(MAX_INPUT_NOTES).sum::(); - - Ok(BalanceInfo { value, spendable }) - } - - /// Gets the stake and the expiration of said stake for a key. - pub fn get_stake( - &self, - sk_index: u64, - ) -> Result> { - let sk = self - .store - .retrieve_sk(sk_index) - .map_err(Error::from_store_err)?; - - let pk = PublicKey::from(&sk); - - let s = self.state.fetch_stake(&pk).map_err(Error::from_state_err)?; - - Ok(s) - } -} - -/// Pick the notes to be used in a transaction from a vector of notes. -/// -/// The notes are picked in a way to maximize the number of notes used, while -/// minimizing the value employed. To do this we sort the notes in ascending -/// value order, and go through each combination in a lexicographic order -/// until we find the first combination whose sum is larger or equal to -/// the given value. If such a slice is not found, an empty vector is returned. -/// -/// Note: it is presupposed that the input notes contain enough balance to cover -/// the given `value`. -fn pick_notes( - value: u64, - notes_and_values: Vec<(Note, u64, JubJubScalar)>, -) -> Vec<(Note, u64, JubJubScalar)> { - let mut notes_and_values = notes_and_values; - let len = notes_and_values.len(); - - if len <= MAX_INPUT_NOTES { - return notes_and_values; - } - - notes_and_values.sort_by(|(_, aval, _), (_, bval, _)| aval.cmp(bval)); - - pick_lexicographic(notes_and_values.len(), |indices| { - indices - .iter() - .map(|index| notes_and_values[*index].1) - .sum::() - >= value - }) - .map(|indices| { - indices - .into_iter() - .map(|index| notes_and_values[index]) - .collect() - }) - .unwrap_or_default() -} - -fn pick_lexicographic bool>( - max_len: usize, - is_valid: F, -) -> Option<[usize; MAX_INPUT_NOTES]> { - let mut indices = [0; MAX_INPUT_NOTES]; - indices - .iter_mut() - .enumerate() - .for_each(|(i, index)| *index = i); - - loop { - if is_valid(&indices) { - return Some(indices); - } - - let mut i = MAX_INPUT_NOTES - 1; - - while indices[i] == i + max_len - MAX_INPUT_NOTES { - if i > 0 { - i -= 1; - } else { - break; - } - } - - indices[i] += 1; - for j in i + 1..MAX_INPUT_NOTES { - indices[j] = indices[j - 1] + 1; - } - - if indices[MAX_INPUT_NOTES - 1] == max_len { - break; - } - } - - None -} - -/// Creates a signature compatible with what the stake contract expects for a -/// stake transaction. -/// -/// The counter is the number of transactions that have been sent to the -/// transfer contract by a given key, and is reported in `StakeInfo`. -fn stake_sign( - sk: &SecretKey, - pk: &PublicKey, - counter: u64, - value: u64, -) -> Signature { - let mut msg = Vec::with_capacity(u64::SIZE + u64::SIZE); - - msg.extend(counter.to_bytes()); - msg.extend(value.to_bytes()); - - sk.sign(pk, &msg) -} - -/// Creates a signature compatible with what the stake contract expects for a -/// unstake transaction. -/// -/// The counter is the number of transactions that have been sent to the -/// transfer contract by a given key, and is reported in `StakeInfo`. -fn unstake_sign( - sk: &SecretKey, - pk: &PublicKey, - counter: u64, - note: Note, -) -> Signature { - let mut msg = Vec::with_capacity(u64::SIZE + Note::SIZE); - - msg.extend(counter.to_bytes()); - msg.extend(note.to_bytes()); - - sk.sign(pk, &msg) -} - -/// Creates a signature compatible with what the stake contract expects for a -/// withdraw transaction. -/// -/// The counter is the number of transactions that have been sent to the -/// transfer contract by a given key, and is reported in `StakeInfo`. -fn withdraw_sign( - sk: &SecretKey, - pk: &PublicKey, - counter: u64, - address: StealthAddress, - nonce: BlsScalar, -) -> Signature { - let mut msg = - Vec::with_capacity(u64::SIZE + StealthAddress::SIZE + BlsScalar::SIZE); - - msg.extend(counter.to_bytes()); - msg.extend(address.to_bytes()); - msg.extend(nonce.to_bytes()); - - sk.sign(pk, &msg) -} - -/// Creates a signature compatible with what the stake contract expects for a -/// ADD_ALLOWLIST transaction. -/// -/// The counter is the number of transactions that have been sent to the -/// transfer contract by a given key, and is reported in `StakeInfo`. -fn allow_sign( - sk: &SecretKey, - pk: &PublicKey, - counter: u64, - staker: &PublicKey, -) -> Signature { - let mut msg = Vec::with_capacity(u64::SIZE + PublicKey::SIZE); - - msg.extend(counter.to_bytes()); - msg.extend(staker.to_bytes()); - - sk.sign(pk, &msg) -} - -/// Generates an obfuscated note for the given public spend key. -fn generate_obfuscated_note( - rng: &mut Rng, - psk: &PublicSpendKey, - value: u64, - nonce: BlsScalar, -) -> (Note, JubJubScalar) { - let r = JubJubScalar::random(rng); - let blinder = JubJubScalar::random(rng); - - ( - Note::deterministic( - NoteType::Obfuscated, - &r, - nonce, - psk, - value, - blinder, - ), - blinder, - ) -} - -#[cfg(test)] -mod tests { - use rand::rngs::StdRng; - use rand_core::SeedableRng; - - use super::*; - - fn gen_notes(values: &[u64]) -> Vec<(Note, u64, JubJubScalar)> { - let mut rng = StdRng::seed_from_u64(0xbeef); - - let ssk = SecretSpendKey::random(&mut rng); - let psk = ssk.public_spend_key(); - - let mut notes_and_values = Vec::with_capacity(values.len()); - - for value in values { - let note = Note::transparent(&mut rng, &psk, *value); - let blinder = JubJubScalar::random(&mut rng); - - notes_and_values.push((note, *value, blinder)); - } - - notes_and_values - } - - #[test] - fn note_picking_none() { - let values = [2, 1, 4, 3, 5, 7, 6]; - - let notes_and_values = gen_notes(&values); - - let picked = pick_notes(100, notes_and_values); - - assert_eq!(picked.len(), 0); - } - - #[test] - fn note_picking_1() { - let values = [1]; - - let notes_and_values = gen_notes(&values); - - let picked = pick_notes(1, notes_and_values); - assert_eq!(picked.len(), 1); - } - - #[test] - fn note_picking_2() { - let values = [1, 2]; - - let notes_and_values = gen_notes(&values); - - let picked = pick_notes(2, notes_and_values); - assert_eq!(picked.len(), 2); - } - - #[test] - fn note_picking_3() { - let values = [1, 3, 2]; - - let notes_and_values = gen_notes(&values); - - let picked = pick_notes(2, notes_and_values); - assert_eq!(picked.len(), 3); - } - - #[test] - fn note_picking_4() { - let values = [4, 2, 1, 3]; - - let notes_and_values = gen_notes(&values); - - let picked = pick_notes(2, notes_and_values); - assert_eq!(picked.len(), 4); - } - - #[test] - fn note_picking_4_plus() { - let values = [2, 1, 4, 3, 5, 7, 6]; - - let notes_and_values = gen_notes(&values); - - let picked = pick_notes(20, notes_and_values); - - assert_eq!(picked.len(), 4); - assert_eq!(picked.iter().map(|v| v.1).sum::(), 20); - } -} diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 0000000..e4af0f8 --- /dev/null +++ b/src/key.rs @@ -0,0 +1,40 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Utilities to derive keys from the seed. + +use crate::utils; + +use dusk_bls12_381_sign::SecretKey; +use dusk_pki::{SecretSpendKey, ViewKey}; + +/// Generates a secret spend key from its seed and index. +/// +/// First the `seed` and then the little-endian representation of the key's +/// `index` are passed through SHA-256. A constant is then mixed in and the +/// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is +/// subsequently used to generate the key. +pub fn derive_ssk(seed: &[u8; utils::RNG_SEED], index: u64) -> SecretSpendKey { + SecretSpendKey::random(&mut utils::rng_with_index(seed, index)) +} + +/// Generates a secret key from its seed and index. +/// +/// First the `seed` and then the little-endian representation of the key's +/// `index` are passed through SHA-256. A constant is then mixed in and the +/// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is +/// subsequently used to generate the key. +pub fn derive_sk(seed: &[u8; utils::RNG_SEED], index: u64) -> SecretKey { + SecretKey::random(&mut utils::rng_with_index(seed, index)) +} + +/// Generates a view key from its seed and index. +/// +/// The secret spend key is derived from [derive_ssk], and then the key is +/// generated via [SecretSpendKey::view_key]. +pub fn derive_vk(seed: &[u8; utils::RNG_SEED], index: u64) -> ViewKey { + derive_ssk(seed, index).view_key() +} diff --git a/src/lib.rs b/src/lib.rs index 8269f65..108abf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,287 +4,105 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -//! The wallet specification. - +#![cfg_attr(target_family = "wasm", no_std)] #![deny(missing_docs)] -#![deny(clippy::all)] -#![allow(clippy::result_large_err)] -// #![no_std] +#![doc = include_str!("../README.md")] -#[macro_use] extern crate alloc; -#[cfg(target_family = "wasm")] -mod ffi; - -mod imp; -mod tx; - use alloc::vec::Vec; -use dusk_bls12_381_sign::{PublicKey, SecretKey}; -use dusk_bytes::{DeserializableSlice, Serializable, Write}; -use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubScalar}; -use dusk_pki::{SecretSpendKey, ViewKey}; -use dusk_plonk::proof_system::Proof; -use dusk_schnorr::Signature; -use phoenix_core::{Crossover, Fee, Note}; -use poseidon_merkle::Opening as PoseidonOpening; -use rand_chacha::ChaCha12Rng; -use rand_core::SeedableRng; -use sha2::{Digest, Sha256}; - -pub use imp::*; -pub use phoenix_core::Transaction; -pub use tx::{UnprovenTransaction, UnprovenTransactionInput}; - -use phoenix_core::transaction::*; -pub use rusk_abi::POSEIDON_TREE_DEPTH; - -/// The maximum size of call data. -pub const MAX_CALL_SIZE: usize = rusk_abi::ARGBUF_LEN; - -/// Stores the cryptographic material necessary to derive cryptographic keys. -pub trait Store { - /// The error type returned from the store. - type Error; - - /// Retrieves the seed used to derive keys. - fn get_seed(&self) -> Result<[u8; 64], Self::Error>; - - /// Retrieves a derived secret spend key from the store. - /// - /// The provided implementation simply gets the seed and regenerates the key - /// every time with [`generate_ssk`]. It may be reimplemented to - /// provide a cache for keys, or implement a different key generation - /// algorithm. - fn retrieve_ssk(&self, index: u64) -> Result { - let seed = self.get_seed()?; - Ok(derive_ssk(&seed, index)) - } - - /// Retrieves a derived secret key from the store. - /// - /// The provided implementation simply gets the seed and regenerates the key - /// every time with [`generate_sk`]. It may be reimplemented to - /// provide a cache for keys, or implement a different key generation - /// algorithm. - fn retrieve_sk(&self, index: u64) -> Result { - let seed = self.get_seed()?; - Ok(derive_sk(&seed, index)) - } -} - -/// Generates a secret spend key from its seed and index. -/// -/// First the `seed` and then the little-endian representation of the key's -/// `index` are passed through SHA-256. A constant is then mixed in and the -/// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is -/// subsequently used to generate the key. -pub fn derive_ssk(seed: &[u8; 64], index: u64) -> SecretSpendKey { - let mut hash = Sha256::new(); - - hash.update(seed); - hash.update(index.to_le_bytes()); - hash.update(b"SSK"); - - let hash = hash.finalize().into(); - let mut rng = ChaCha12Rng::from_seed(hash); - - SecretSpendKey::random(&mut rng) -} - -/// Generates a secret key from its seed and index. -/// -/// First the `seed` and then the little-endian representation of the key's -/// `index` are passed through SHA-256. A constant is then mixed in and the -/// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is -/// subsequently used to generate the key. -pub fn derive_sk(seed: &[u8; 64], index: u64) -> SecretKey { - let mut hash = Sha256::new(); - - hash.update(seed); - hash.update(index.to_le_bytes()); - hash.update(b"SK"); - - let hash = hash.finalize().into(); - let mut rng = ChaCha12Rng::from_seed(hash); - - SecretKey::random(&mut rng) -} - -/// Types that are client of the prover. -pub trait ProverClient { - /// Error returned by the node client. - type Error; - - /// Requests that a node prove the given transaction and later propagates it - fn compute_proof_and_propagate( - &self, - utx: &UnprovenTransaction, - ) -> Result; - - /// Requests an STCT proof. - fn request_stct_proof( - &self, - fee: &Fee, - crossover: &Crossover, - value: u64, - blinder: JubJubScalar, - address: BlsScalar, - signature: Signature, - ) -> Result; - - /// Request a WFCT proof. - fn request_wfct_proof( - &self, - commitment: JubJubAffine, - value: u64, - blinder: JubJubScalar, - ) -> Result; -} - -/// Block height representation -pub type BlockHeight = u64; - -/// Tuple containing Note and Block height -pub type EnrichedNote = (Note, BlockHeight); - -/// Types that are clients of the state API. -pub trait StateClient { - /// Error returned by the node client. - type Error; - - /// Find notes for a view key. - fn fetch_notes( - &self, - vk: &ViewKey, - ) -> Result, Self::Error>; - - /// Fetch the current anchor of the state. - fn fetch_anchor(&self) -> Result; - - /// Asks the node to return the nullifiers that already exist from the given - /// nullifiers. - fn fetch_existing_nullifiers( - &self, - nullifiers: &[BlsScalar], - ) -> Result, Self::Error>; - - /// Queries the node to find the opening for a specific note. - fn fetch_opening( - &self, - note: &Note, - ) -> Result, Self::Error>; - - /// Queries the node for the stake of a key. If the key has no stake, a - /// `Default` stake info should be returned. - fn fetch_stake(&self, pk: &PublicKey) -> Result; +use core::mem; + +use bytecheck::CheckBytes; +use rkyv::{Archive, Deserialize, Serialize}; + +pub mod ffi; +pub mod key; +pub mod tx; +pub mod utils; + +/// The maximum number of keys to derive when attempting to decrypt a note. +pub const MAX_KEY: usize = 24; + +/// The maximum allocated buffer for rkyv serialization. +pub const MAX_LEN: usize = rusk_abi::ARGBUF_LEN; + +/// The arguments of the balance function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct BalanceArgs { + /// Seed used to derive the keys of the wallet. + pub seed: [u8; utils::RNG_SEED], + /// A rkyv serialized [Vec]; all notes should + /// have their keys derived from `seed`. + pub notes: Vec, } -/// Information about the balance of a particular key. -#[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)] -pub struct BalanceInfo { - /// The total value of the balance. +/// The response of the balance function. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, +)] +#[archive_attr(derive(CheckBytes))] +pub struct BalanceResponse { + /// Status of the execution + pub success: bool, + /// Total computed balance pub value: u64, - /// The maximum _spendable_ value in a single transaction. This is - /// different from `value` since there is a maximum number of notes one can - /// spend. - pub spendable: u64, } -impl Serializable<16> for BalanceInfo { - type Error = dusk_bytes::Error; - - fn from_bytes(buf: &[u8; Self::SIZE]) -> Result - where - Self: Sized, - { - let mut reader = &buf[..]; - - let value = u64::from_reader(&mut reader)?; - let spendable = u64::from_reader(&mut reader)?; - - Ok(Self { value, spendable }) - } - - #[allow(unused_must_use)] - fn to_bytes(&self) -> [u8; Self::SIZE] { - let mut buf = [0u8; Self::SIZE]; - let mut writer = &mut buf[..]; - - writer.write(&self.value.to_bytes()); - writer.write(&self.spendable.to_bytes()); - - buf - } +impl BalanceResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); } -/// The stake of a particular key. -#[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)] -pub struct StakeInfo { - /// The value and eligibility of the stake, in that order. - pub amount: Option<(u64, u64)>, - /// The reward available for withdrawal. - pub reward: u64, - /// Signature counter. - pub counter: u64, +/// The arguments of the execute function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct ExecuteArgs { + /// Seed used to derive the keys of the wallet. + pub seed: [u8; utils::RNG_SEED], + /// Seed used to derive the entropy for the notes. + pub rng_seed: [u8; utils::RNG_SEED], + /// A rkyv serialized [Vec] to be used as inputs + pub inputs: Vec, + /// A rkyv serialized [Vec] to open the inputs to a Merkle + /// root. + pub openings: Vec, + /// A rkyv serialize [dusk_pki::PublicSpendKey] to whom the remainder + /// balance will be refunded + pub refund: Vec, + /// A rkyv serialized [Option] to define the receiver. + pub output: Vec, + /// The [phoenix_core::Crossover] value; will be skipped if `0`. + pub crossover: u64, + /// The gas limit of the transaction. + pub gas_limit: u64, + /// The gas price per unit for the transaction. + pub gas_price: u64, + /// A rkyv serialized [Option] to perform contract calls. + pub call: Vec, } -impl From for StakeInfo { - fn from(data: StakeData) -> Self { - StakeInfo { - amount: data.amount, - reward: data.reward, - counter: data.counter, - } - } +/// The response of the execute function. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, +)] +#[archive_attr(derive(CheckBytes))] +pub struct ExecuteResponse { + /// Status of the execution + pub success: bool, + /// The pointer to a rkyv serialized [Vec>] + /// containing the notes that weren't used. + pub unspent_ptr: u64, + /// The length of the rkyv serialized `unspent_ptr`. + pub unspent_len: u64, + /// The pointer to a rkyv serialized [tx::UnspentTransaction]. + pub tx_ptr: u64, + /// The length of the rkyv serialized `tx_ptr`. + pub tx_len: u64, } -impl Serializable<32> for StakeInfo { - type Error = dusk_bytes::Error; - - /// Deserializes in the same order as defined in [`to_bytes`]. If the - /// deserialized value is 0, then `amount` will be `None`. This means that - /// the eligibility value is left loose, and could be any number when value - /// is 0. - fn from_bytes(buf: &[u8; Self::SIZE]) -> Result - where - Self: Sized, - { - let mut reader = &buf[..]; - - let value = u64::from_reader(&mut reader)?; - let eligibility = u64::from_reader(&mut reader)?; - let reward = u64::from_reader(&mut reader)?; - let counter = u64::from_reader(&mut reader)?; - - let amount = match value > 0 { - true => Some((value, eligibility)), - false => None, - }; - - Ok(Self { - amount, - reward, - counter, - }) - } - - /// Serializes the amount and the eligibility first, and then the reward and - /// the counter. If `amount` is `None`, and since a stake of no value should - /// not be possible, the first 16 bytes are filled with zeros. - #[allow(unused_must_use)] - fn to_bytes(&self) -> [u8; Self::SIZE] { - let mut buf = [0u8; Self::SIZE]; - let mut writer = &mut buf[..]; - - let (value, eligibility) = self.amount.unwrap_or_default(); - - writer.write(&value.to_bytes()); - writer.write(&eligibility.to_bytes()); - writer.write(&self.reward.to_bytes()); - writer.write(&self.counter.to_bytes()); - - buf - } +impl ExecuteResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); } diff --git a/src/tx.rs b/src/tx.rs index 578c1ed..ecf9452 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -4,221 +4,249 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::StateClient; +//! Unspent transaction definition. use alloc::string::String; use alloc::vec::Vec; -use dusk_bytes::{ - DeserializableSlice, Error as BytesError, Serializable, Write, +use bytecheck::CheckBytes; +use dusk_jubjub::{ + BlsScalar, JubJubExtended, JubJubScalar, GENERATOR_NUMS_EXTENDED, }; -use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubExtended}; -use dusk_pki::{Ownable, SecretSpendKey}; -use dusk_plonk::prelude::{JubJubScalar, Proof}; +use dusk_pki::{Ownable, PublicSpendKey, SecretSpendKey}; use dusk_schnorr::Proof as SchnorrSig; -use phoenix_core::transaction::Transaction; -use phoenix_core::{Crossover, Fee, Note}; -use poseidon_merkle::Opening as PoseidonOpening; +use phoenix_core::{ + Crossover as PhoenixCrossover, Fee, Note, NoteType, Transaction, +}; use rand_core::{CryptoRng, RngCore}; +use rkyv::{Archive, Deserialize, Serialize}; use rusk_abi::hash::Hasher; -use rusk_abi::{ContractId, CONTRACT_ID_BYTES, POSEIDON_TREE_DEPTH}; +use rusk_abi::{ContractId, POSEIDON_TREE_DEPTH}; + +/// Chosen arity for the Notes tree implementation. +pub const POSEIDON_TREE_ARITY: usize = 4; + +/// The Merkle Opening used in Rusk. +pub type Opening = + poseidon_merkle::Opening<(), POSEIDON_TREE_DEPTH, POSEIDON_TREE_ARITY>; + +/// A preliminary input to a transaction that is yet to be proven. +pub struct PreInput<'a> { + /// Input note to be used in the transaction. + pub note: Note, + /// Opening from the `input` to the Merkle root of the state. + pub opening: Opening, + /// Decrypted value of the input note. + pub value: u64, + /// Secret key to generate the nullifier of the input note. + pub ssk: &'a SecretSpendKey, +} /// An input to a transaction that is yet to be proven. -#[derive(Debug, Clone)] -pub struct UnprovenTransactionInput { - nullifier: BlsScalar, - opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, - note: Note, - value: u64, - blinder: JubJubScalar, - pk_r_prime: JubJubExtended, - sig: SchnorrSig, +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct Input { + /// Nulifier generated from the input note. + pub nullifier: BlsScalar, + /// Opening from the `input` to the Merkle root of the state. + pub opening: Opening, + /// Input note to be used in the transaction. + pub note: Note, + /// Decrypted value of the input note. + pub value: u64, + /// Blinding factor used to construct the note. + pub blinder: JubJubScalar, + /// Stealth address derived from the key of the owner of the note. + pub pk_r_prime: JubJubExtended, + /// Schnorr signature to prove the ownership of the note. + pub sig: SchnorrSig, } -impl UnprovenTransactionInput { - fn new( - rng: &mut Rng, - ssk: &SecretSpendKey, - note: Note, - value: u64, - blinder: JubJubScalar, - opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, - tx_hash: BlsScalar, - ) -> Self { - let nullifier = note.gen_nullifier(ssk); - let sk_r = ssk.sk_r(note.stealth_address()); - let sig = SchnorrSig::new(&sk_r, rng, tx_hash); - - let pk_r_prime = dusk_jubjub::GENERATOR_NUMS_EXTENDED * sk_r.as_ref(); - - Self { - note, - value, - blinder, - sig, - nullifier, - opening, - pk_r_prime, - } - } - - /// Serialize the input to a variable size byte buffer. - pub fn to_var_bytes(&self) -> Vec { - let affine_pkr = JubJubAffine::from(&self.pk_r_prime); - - let opening_bytes = rkyv::to_bytes::<_, 256>(&self.opening) - .expect("Rkyv serialization should always succeed for an opening") - .to_vec(); - - let mut bytes = Vec::with_capacity( - BlsScalar::SIZE - + Note::SIZE - + JubJubAffine::SIZE - + SchnorrSig::SIZE - + u64::SIZE - + JubJubScalar::SIZE - + opening_bytes.len(), - ); - - bytes.extend_from_slice(&self.nullifier.to_bytes()); - bytes.extend_from_slice(&self.note.to_bytes()); - bytes.extend_from_slice(&self.value.to_bytes()); - bytes.extend_from_slice(&self.blinder.to_bytes()); - bytes.extend_from_slice(&affine_pkr.to_bytes()); - bytes.extend_from_slice(&self.sig.to_bytes()); - bytes.extend(opening_bytes); - - bytes - } - - /// Deserializes the the input from bytes. - pub fn from_slice(buf: &[u8]) -> Result { - let mut bytes = buf; - - let nullifier = BlsScalar::from_reader(&mut bytes)?; - let note = Note::from_reader(&mut bytes)?; - let value = u64::from_reader(&mut bytes)?; - let blinder = JubJubScalar::from_reader(&mut bytes)?; - let pk_r_prime = - JubJubExtended::from(JubJubAffine::from_reader(&mut bytes)?); - let sig = SchnorrSig::from_reader(&mut bytes)?; - - // `to_vec` is required here otherwise `rkyv` will throw an alignment - // error - #[allow(clippy::unnecessary_to_owned)] - let opening = rkyv::from_bytes(&bytes.to_vec()) - .map_err(|_| BytesError::InvalidData)?; - - Ok(Self { - note, - value, - blinder, - sig, - nullifier, - opening, - pk_r_prime, - }) - } - - /// Returns the nullifier of the input. - pub fn nullifier(&self) -> BlsScalar { - self.nullifier - } - - /// Returns the opening of the input. - pub fn opening(&self) -> &PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4> { - &self.opening - } - - /// Returns the note of the input. - pub fn note(&self) -> &Note { - &self.note - } - - /// Returns the value of the input. - pub fn value(&self) -> u64 { - self.value - } +/// A preliminary output to a transaction that is yet to be proven. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct OutputValue { + /// Type of the output note to be used in the transaction. + pub r#type: NoteType, + /// Value of the output. + pub value: u64, + /// Public key that will receive the note as spendable input. + pub receiver: PublicSpendKey, + /// Nonce/reference to be attached to the note. + pub ref_id: u64, +} - /// Returns the blinding factor of the input. - pub fn blinding_factor(&self) -> JubJubScalar { - self.blinder - } +/// An output to a transaction that is yet to be proven. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct Output { + /// Computed output note to be used in the transaction. + pub note: Note, + /// Decrypted value of the output note. + pub value: u64, + /// Blinding factor used to construct the note. + pub blinder: JubJubScalar, +} - /// Returns the input's pk_r'. - pub fn pk_r_prime(&self) -> JubJubExtended { - self.pk_r_prime - } +/// A crossover to a transaction that is yet to be proven. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct Crossover { + /// Crossover value to be used in inter-contract calls. + pub crossover: PhoenixCrossover, + /// Value of the crossover. + pub value: u64, + /// Blinding factor used to construct the crossover. + pub blinder: JubJubScalar, +} - /// Returns the input's signature. - pub fn signature(&self) -> &SchnorrSig { - &self.sig - } +/// A call data payload to a transaction that is yet to be proven. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct CallData { + /// Contract ID to be called. + pub contract: ContractId, + /// Name of the method to be called. + pub method: String, + /// Payload of the call to be sent to the contract module. + pub payload: Vec, } -/// A transaction that is yet to be proven. The purpose of this is solely to -/// send to the node to perform a circuit proof. -#[derive(Debug, Clone)] +/// A transaction that is yet to be proven. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] pub struct UnprovenTransaction { - inputs: Vec, - outputs: Vec<(Note, u64, JubJubScalar)>, - anchor: BlsScalar, - fee: Fee, - crossover: Option<(Crossover, u64, JubJubScalar)>, - call: Option<(ContractId, String, Vec)>, + /// Inputs to the transaction. + pub inputs: Vec, + /// Outputs to the transaction. + pub outputs: Vec, + /// Merkle root of the state for the inputs openings. + pub anchor: BlsScalar, + /// Fee setup for the transaction. + pub fee: Fee, + /// Crossover value for inter-contract calls. + pub crossover: Option, + /// Call data payload for contract calls. + pub call: Option, } impl UnprovenTransaction { - /// Creates a transaction that conforms to the transfer contract. - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( + /// Creates a new unproven transaction from the arguments. + /// + /// The transaction can be sent to a prover service and it contains all the + /// data required to generate a ZK proof of validity. + pub fn new<'a, Rng, I, O>( rng: &mut Rng, - state: &SC, - sender: &SecretSpendKey, - inputs: Vec<(Note, u64, JubJubScalar)>, - outputs: Vec<(Note, u64, JubJubScalar)>, - fee: Fee, - crossover: Option<(Crossover, u64, JubJubScalar)>, - call: Option<(ContractId, String, Vec)>, - ) -> Result { - let nullifiers: Vec = inputs - .iter() - .map(|(note, _, _)| note.gen_nullifier(sender)) - .collect(); - - let mut openings = Vec::with_capacity(inputs.len()); - for (note, _, _) in &inputs { - let opening = state.fetch_opening(note)?; - openings.push(opening); - } - - let anchor = state.fetch_anchor()?; - - let hash_outputs: Vec = outputs.iter().map(|o| o.0).collect(); - let hash_crossover = crossover.map(|c| c.0); + inputs: I, + outputs: O, + refund: &'a PublicSpendKey, + gas_limit: u64, + gas_price: u64, + crossover: u64, + call: Option, + ) -> Option + where + Rng: RngCore + CryptoRng, + I: IntoIterator>, + O: IntoIterator, + { + let (nullifiers, inputs): (Vec<_>, Vec<_>) = inputs + .into_iter() + .map(|i| { + let nullifier = i.note.gen_nullifier(i.ssk); + (nullifier, i) + }) + .unzip(); - let hash_call = call.clone().map(|c| (c.0.to_bytes(), c.1, c.2)); - let hash_bytes = Transaction::hash_input_bytes_from_components( + let anchor = inputs.first().map(|i| i.opening.root().hash)?; + let (output_notes, outputs): (Vec<_>, Vec<_>) = outputs + .into_iter() + .map(|o| { + let r = JubJubScalar::random(rng); + let blinder = JubJubScalar::random(rng); + let nonce = BlsScalar::from(o.ref_id); + let note = Note::deterministic( + o.r#type, + &r, + nonce, + &o.receiver, + o.value, + blinder, + ); + Output { + note, + value: o.value, + blinder, + } + }) + .map(|o| (o.note.clone(), o)) + .unzip(); + + let call_phoenix = call.as_ref().map(|c| { + (c.contract.to_bytes(), c.method.clone(), c.payload.clone()) + }); + + let fee = Fee::new(rng, gas_limit, gas_price, refund); + + let crossover = (crossover > 0).then(|| { + let blinder = JubJubScalar::random(rng); + let (_, crossover_note) = + Note::obfuscated(rng, refund, crossover, blinder) + .try_into() + .expect("Obfuscated notes should always yield crossovers"); + Crossover { + crossover: crossover_note, + value: crossover, + blinder, + } + }); + + let tx_hash = Transaction::hash_input_bytes_from_components( &nullifiers, - &hash_outputs, + &output_notes, &anchor, &fee, - &hash_crossover, - &hash_call, + &crossover.as_ref().map(|c| c.crossover), + &call_phoenix, ); - let hash = Hasher::digest(hash_bytes); + let tx_hash = Hasher::digest(tx_hash); - let inputs: Vec = inputs + let inputs = inputs .into_iter() - .zip(openings.into_iter()) - .map(|((note, value, blinder), opening)| { - UnprovenTransactionInput::new( - rng, sender, note, value, blinder, opening, hash, - ) - }) - .collect(); - - Ok(Self { + .zip(nullifiers.into_iter()) + .map( + |( + PreInput { + note, + opening, + value, + ssk, + }, + nullifier, + )| { + let vk = ssk.view_key(); + let sk_r = ssk.sk_r(note.stealth_address()); + + let blinder = + note.blinding_factor(Some(&vk)).map_err(|_| ())?; + let pk_r_prime = GENERATOR_NUMS_EXTENDED * sk_r.as_ref(); + let sig = SchnorrSig::new(&sk_r, rng, tx_hash); + + Ok(Input { + nullifier, + opening, + note, + value, + blinder, + pk_r_prime, + sig, + }) + }, + ) + .collect::, ()>>() + .ok()?; + + Some(UnprovenTransaction { inputs, outputs, anchor, @@ -227,301 +255,4 @@ impl UnprovenTransaction { call, }) } - - /// Consumes self and a proof to generate a transaction. - pub fn prove(self, proof: Proof) -> Transaction { - Transaction { - anchor: self.anchor, - nullifiers: self - .inputs - .into_iter() - .map(|input| input.nullifier) - .collect(), - outputs: self - .outputs - .into_iter() - .map(|(note, _, _)| note) - .collect(), - fee: self.fee, - crossover: self.crossover.map(|c| c.0), - proof: proof.to_bytes().to_vec(), - call: self.call.map(|c| (c.0.to_bytes(), c.1, c.2)), - } - } - - /// Serialize the transaction to a variable length byte buffer. - #[allow(unused_must_use)] - pub fn to_var_bytes(&self) -> Vec { - let serialized_inputs: Vec> = self - .inputs - .iter() - .map(UnprovenTransactionInput::to_var_bytes) - .collect(); - let num_inputs = self.inputs.len(); - let total_input_len = serialized_inputs - .iter() - .fold(0, |len, input| len + input.len()); - - let serialized_outputs: Vec< - [u8; Note::SIZE + u64::SIZE + JubJubScalar::SIZE], - > = self - .outputs - .iter() - .map(|(note, value, blinder)| { - let mut buf = [0; Note::SIZE + u64::SIZE + JubJubScalar::SIZE]; - - buf[..Note::SIZE].copy_from_slice(¬e.to_bytes()); - buf[Note::SIZE..Note::SIZE + u64::SIZE] - .copy_from_slice(&value.to_bytes()); - buf[Note::SIZE + u64::SIZE - ..Note::SIZE + u64::SIZE + JubJubScalar::SIZE] - .copy_from_slice(&blinder.to_bytes()); - - buf - }) - .collect(); - let num_outputs = self.outputs.len(); - let total_output_len = serialized_outputs - .iter() - .fold(0, |len, output| len + output.len()); - - let size = u64::SIZE - + num_inputs * u64::SIZE - + total_input_len - + u64::SIZE - + total_output_len - + BlsScalar::SIZE - + Fee::SIZE - + u64::SIZE - + self.crossover.map_or(0, |_| { - Crossover::SIZE + u64::SIZE + JubJubScalar::SIZE - }) - + u64::SIZE - + self - .call - .as_ref() - .map(|(_, cname, cdata)| { - CONTRACT_ID_BYTES + u64::SIZE + cname.len() + cdata.len() - }) - .unwrap_or(0); - - let mut buf = vec![0; size]; - let mut writer = &mut buf[..]; - - writer.write(&(num_inputs as u64).to_bytes()); - for sinput in serialized_inputs { - writer.write(&(sinput.len() as u64).to_bytes()); - writer.write(&sinput); - } - - writer.write(&(num_outputs as u64).to_bytes()); - for soutput in serialized_outputs { - writer.write(&soutput); - } - - writer.write(&self.anchor.to_bytes()); - writer.write(&self.fee.to_bytes()); - - write_crossover_value_blinder(&mut writer, self.crossover); - write_optional_call(&mut writer, &self.call); - - buf - } - - /// Deserialize the transaction from a bytes buffer. - pub fn from_slice(buf: &[u8]) -> Result { - let mut buffer = buf; - - let num_inputs = u64::from_reader(&mut buffer)?; - let mut inputs = Vec::with_capacity(num_inputs as usize); - for _ in 0..num_inputs { - let size = u64::from_reader(&mut buffer)? as usize; - inputs.push(UnprovenTransactionInput::from_slice(&buffer[..size])?); - buffer = &buffer[size..]; - } - - let num_outputs = u64::from_reader(&mut buffer)?; - let mut outputs = Vec::with_capacity(num_outputs as usize); - for _ in 0..num_outputs { - let note = Note::from_reader(&mut buffer)?; - let value = u64::from_reader(&mut buffer)?; - let blinder = JubJubScalar::from_reader(&mut buffer)?; - - outputs.push((note, value, blinder)); - } - - let anchor = BlsScalar::from_reader(&mut buffer)?; - let fee = Fee::from_reader(&mut buffer)?; - - let crossover = read_crossover_value_blinder(&mut buffer)?; - - let call = read_optional_call(&mut buffer)?; - - Ok(Self { - inputs, - outputs, - anchor, - fee, - crossover, - call, - }) - } - - /// Returns the hash of the transaction. - pub fn hash(&self) -> BlsScalar { - let nullifiers: Vec = - self.inputs.iter().map(|input| input.nullifier).collect(); - - let hash_outputs: Vec = - self.outputs.iter().map(|(note, _, _)| *note).collect(); - let hash_crossover = self.crossover.map(|c| c.0); - let hash_bytes = self.call.clone().map(|c| (c.0.to_bytes(), c.1, c.2)); - - Hasher::digest(Transaction::hash_input_bytes_from_components( - &nullifiers, - &hash_outputs, - &self.anchor, - &self.fee, - &hash_crossover, - &hash_bytes, - )) - } - - /// Returns the inputs to the transaction. - pub fn inputs(&self) -> &[UnprovenTransactionInput] { - &self.inputs - } - - /// Returns the outputs of the transaction. - pub fn outputs(&self) -> &[(Note, u64, JubJubScalar)] { - &self.outputs - } - - /// Returns the anchor of the transaction. - pub fn anchor(&self) -> BlsScalar { - self.anchor - } - - /// Returns the fee of the transaction. - pub fn fee(&self) -> &Fee { - &self.fee - } - - /// Returns the crossover of the transaction. - pub fn crossover(&self) -> Option<&(Crossover, u64, JubJubScalar)> { - self.crossover.as_ref() - } - - /// Returns the call of the transaction. - pub fn call(&self) -> Option<&(ContractId, String, Vec)> { - self.call.as_ref() - } -} - -/// Writes an optional call into the writer, prepending it with a `u64` denoting -/// if it is present or not. This should be called at the end of writing other -/// fields since it doesn't write any information about the length of the call -/// data. -fn write_optional_call( - writer: &mut W, - call: &Option<(ContractId, String, Vec)>, -) -> Result<(), BytesError> { - match call { - Some((cid, cname, cdata)) => { - writer.write(&1_u64.to_bytes())?; - - writer.write(cid.as_bytes())?; - - let cname_len = cname.len() as u64; - writer.write(&cname_len.to_bytes())?; - writer.write(cname.as_bytes())?; - - writer.write(cdata)?; - } - None => { - writer.write(&0_u64.to_bytes())?; - } - }; - - Ok(()) -} - -/// Reads an optional call from the given buffer. This should be called at the -/// end of parsing other fields since it consumes the entirety of the buffer. -fn read_optional_call( - buffer: &mut &[u8], -) -> Result)>, BytesError> { - let mut call = None; - - if u64::from_reader(buffer)? != 0 { - let buf_len = buffer.len(); - - // needs to be at least the size of a contract ID and have some call - // data. - if buf_len < CONTRACT_ID_BYTES { - return Err(BytesError::BadLength { - found: buf_len, - expected: CONTRACT_ID_BYTES, - }); - } - let (mid_buffer, mut buffer_left) = { - let (buf, left) = buffer.split_at(CONTRACT_ID_BYTES); - - let mut mid_buf = [0u8; CONTRACT_ID_BYTES]; - mid_buf.copy_from_slice(buf); - - (mid_buf, left) - }; - - let contract_id = ContractId::from(mid_buffer); - - let buffer = &mut buffer_left; - - let cname_len = u64::from_reader(buffer)?; - let (cname_bytes, buffer_left) = buffer.split_at(cname_len as usize); - - let cname = String::from_utf8(cname_bytes.to_vec()) - .map_err(|_| BytesError::InvalidData)?; - - let call_data = Vec::from(buffer_left); - call = Some((contract_id, cname, call_data)); - } - - Ok(call) -} - -fn write_crossover_value_blinder( - writer: &mut W, - crossover: Option<(Crossover, u64, JubJubScalar)>, -) -> Result<(), BytesError> { - match crossover { - Some((crossover, value, blinder)) => { - writer.write(&1_u64.to_bytes())?; - writer.write(&crossover.to_bytes())?; - writer.write(&value.to_bytes())?; - writer.write(&blinder.to_bytes())?; - } - None => { - writer.write(&0_u64.to_bytes())?; - } - } - - Ok(()) -} - -/// Reads an optional crossover from the given buffer. -fn read_crossover_value_blinder( - buffer: &mut &[u8], -) -> Result, BytesError> { - let ser = match u64::from_reader(buffer)? { - 0 => None, - _ => { - let crossover = Crossover::from_reader(buffer)?; - let value = u64::from_reader(buffer)?; - let blinder = JubJubScalar::from_reader(buffer)?; - Some((crossover, value, blinder)) - } - }; - - Ok(ser) } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5b67f2b --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,145 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Misc utilities required by the library implementation. + +use crate::tx; + +use alloc::vec::Vec; + +use phoenix_core::Note; +use rand_chacha::ChaCha12Rng; +use rand_core::SeedableRng; +use sha2::{Digest, Sha256}; + +/// Length of the seed of the generated rng. +pub const RNG_SEED: usize = 64; + +/// Creates a secure RNG from a seed. +pub fn rng(seed: &[u8; RNG_SEED]) -> ChaCha12Rng { + let mut hash = Sha256::new(); + + hash.update(seed); + hash.update(b"RNG"); + + let hash = hash.finalize().into(); + ChaCha12Rng::from_seed(hash) +} + +/// Creates a secure RNG from a seed with embedded index. +pub fn rng_with_index(seed: &[u8; RNG_SEED], index: u64) -> ChaCha12Rng { + let mut hash = Sha256::new(); + + hash.update(seed); + hash.update(index.to_le_bytes()); + hash.update(b"INDEX"); + + let hash = hash.finalize().into(); + ChaCha12Rng::from_seed(hash) +} + +/// Perform a knapsack algorithm to define the notes to be used as input. +/// +/// Returns a tuple containing (unspent, inputs). `unspent` contains the notes +/// that are not used. +pub fn knapsack( + mut nodes: Vec<(Note, tx::Opening, u64, usize)>, + target_sum: u64, +) -> Option<(Vec, Vec<(Note, tx::Opening, u64, usize)>)> { + if nodes.is_empty() { + return None; + } + + // TODO implement a knapsack algorithm + // here we do a naive, desc order pick. optimally, we should maximize the + // number of smaller inputs that fits the target sum so we reduce the number + // of available small notes on the wallet. a knapsack implementation is + // optimal for such problems as it can deliver high confidence results + // with moderate memory space. + nodes.sort_by(|a, b| b.2.cmp(&a.2)); + + let mut i = 0; + let mut sum = 0; + while sum < target_sum && i < nodes.len() { + sum = sum.saturating_add(nodes[i].2); + i += 1; + } + + if sum < target_sum { + return None; + } + let unspent = nodes.split_off(i).into_iter().map(|n| n.0).collect(); + + Some((unspent, nodes)) +} + +#[test] +fn knapsack_works() { + use core::mem; + use dusk_jubjub::JubJubScalar; + use dusk_pki::SecretSpendKey; + use rand::{rngs::StdRng, SeedableRng}; + + // openings are not checked here; no point in setting them up properly + let o = unsafe { mem::zeroed() }; + let rng = &mut StdRng::seed_from_u64(0xbeef); + + // sanity check + assert_eq!(knapsack(vec![], 70), None); + + // basic check + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note = Note::obfuscated(rng, &key.public_spend_key(), 100, blinder); + let available = vec![(note, o, 100, 0)]; + let unspent = vec![]; + let inputs = available.clone(); + assert_eq!(knapsack(available, 70), Some((unspent, inputs))); + + // out of balance basic check + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note = Note::obfuscated(rng, &key.public_spend_key(), 100, blinder); + let available = vec![(note, o, 100, 0)]; + assert_eq!(knapsack(available, 101), None); + + // multiple inputs check + // note: this test is checking a naive, simple order-based output + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note1 = Note::obfuscated(rng, &key.public_spend_key(), 100, blinder); + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note2 = Note::obfuscated(rng, &key.public_spend_key(), 500, blinder); + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note3 = Note::obfuscated(rng, &key.public_spend_key(), 300, blinder); + let available = vec![ + (note1.clone(), o, 100, 0), + (note2.clone(), o, 500, 1), + (note3.clone(), o, 300, 2), + ]; + let unspent = vec![note1]; + let inputs = vec![(note2.clone(), o, 500, 1), (note3.clone(), o, 300, 2)]; + assert_eq!(knapsack(available, 600), Some((unspent, inputs))); + + // multiple inputs, out of balance check + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note1 = Note::obfuscated(rng, &key.public_spend_key(), 100, blinder); + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note2 = Note::obfuscated(rng, &key.public_spend_key(), 500, blinder); + let key = SecretSpendKey::random(rng); + let blinder = JubJubScalar::random(rng); + let note3 = Note::obfuscated(rng, &key.public_spend_key(), 300, blinder); + let available = vec![ + (note1.clone(), o, 100, 0), + (note2.clone(), o, 500, 1), + (note3.clone(), o, 300, 2), + ]; + assert_eq!(knapsack(available, 901), None); +} diff --git a/tests/mock.rs b/tests/mock.rs deleted file mode 100644 index 1e5c8d8..0000000 --- a/tests/mock.rs +++ /dev/null @@ -1,350 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. -// -// Copyright (c) DUSK NETWORK. All rights reserved. - -//! Mocks of the traits supplied by the user of the crate.. - -use dusk_bls12_381_sign::PublicKey; -use dusk_jubjub::{BlsScalar, JubJubAffine, JubJubScalar}; -use dusk_pki::{PublicSpendKey, ViewKey}; -use dusk_plonk::prelude::Proof; -use dusk_schnorr::Signature; -use dusk_wallet_core::{ - EnrichedNote, ProverClient, StakeInfo, StateClient, Store, Transaction, - UnprovenTransaction, Wallet, POSEIDON_TREE_DEPTH, -}; -use phoenix_core::{Crossover, Fee, Note, NoteType}; -use poseidon_merkle::{Item, Opening as PoseidonOpening, Tree}; -use rand_core::{CryptoRng, RngCore}; - -fn default_opening() -> PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4> { - // Build a "default" opening - const POS: u64 = 42; - let mut tree = Tree::new(); - tree.insert( - POS, - Item { - hash: BlsScalar::zero(), - data: (), - }, - ); - tree.opening(POS).unwrap() -} - -/// Create a new wallet meant for tests. It includes a client that will always -/// return a random anchor (same every time), and the default opening. -/// -/// The number of notes available is determined by `note_values`. -pub fn mock_wallet( - rng: &mut Rng, - note_values: &[u64], -) -> Wallet { - let store = TestStore::new(rng); - let psk = store.retrieve_ssk(0).unwrap().public_spend_key(); - - let notes = new_notes(rng, &psk, note_values); - let anchor = BlsScalar::random(rng); - let opening = default_opening(); - - let state = TestStateClient::new(notes, anchor, opening); - let prover = TestProverClient; - - Wallet::new(store, state, prover) -} - -/// Create a new wallet equivalent in all ways to `mock_wallet`, but serializing -/// and deserializing a `Transaction` using `rkyv`. -pub fn mock_canon_wallet( - rng: &mut Rng, - note_values: &[u64], -) -> Wallet { - let store = TestStore::new(rng); - let psk = store.retrieve_ssk(0).unwrap().public_spend_key(); - - let notes = new_notes(rng, &psk, note_values); - let anchor = BlsScalar::random(rng); - let opening = default_opening(); - - let state = TestStateClient::new(notes, anchor, opening); - let prover = RkyvProverClient { - prover: TestProverClient, - }; - - Wallet::new(store, state, prover) -} - -/// Create a new wallet equivalent in all ways to `mock_wallet`, but serializing -/// and deserializing an `UnprovenTransaction` using `dusk::bytes`. -pub fn mock_serde_wallet( - rng: &mut Rng, - note_values: &[u64], -) -> Wallet { - let store = TestStore::new(rng); - let psk = store.retrieve_ssk(0).unwrap().public_spend_key(); - - let notes = new_notes(rng, &psk, note_values); - let anchor = BlsScalar::random(rng); - let opening = default_opening(); - - let state = TestStateClient::new(notes, anchor, opening); - let prover = SerdeProverClient { - prover: TestProverClient, - }; - - Wallet::new(store, state, prover) -} - -/// Returns obfuscated notes with the given value. -fn new_notes( - rng: &mut Rng, - psk: &PublicSpendKey, - note_values: &[u64], -) -> Vec { - note_values - .iter() - .map(|val| { - let blinder = JubJubScalar::random(rng); - (Note::new(rng, NoteType::Obfuscated, psk, *val, blinder), 0) - }) - .collect() -} - -/// An in-memory seed store. -#[derive(Debug)] -pub struct TestStore { - seed: [u8; 64], -} - -impl TestStore { - /// Instantiate a new in-memory store with a random seed. - fn new(rng: &mut Rng) -> Self { - let mut seed = [0; 64]; - rng.fill_bytes(&mut seed); - Self { seed } - } -} - -impl Store for TestStore { - type Error = (); - - fn get_seed(&self) -> Result<[u8; 64], Self::Error> { - Ok(self.seed) - } -} - -/// A state client that always returns the same notes, anchor, and opening. -#[derive(Debug, Clone)] -pub struct TestStateClient { - notes: Vec, - anchor: BlsScalar, - opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, -} - -impl TestStateClient { - /// Create a new node given the notes, anchor, and opening we will return. - fn new( - notes: Vec, - anchor: BlsScalar, - opening: PoseidonOpening<(), POSEIDON_TREE_DEPTH, 4>, - ) -> Self { - Self { - notes, - anchor, - opening, - } - } -} - -impl StateClient for TestStateClient { - type Error = (); - - fn fetch_notes( - &self, - _: &ViewKey, - ) -> Result, Self::Error> { - Ok(self.notes.clone()) - } - - fn fetch_anchor(&self) -> Result { - Ok(self.anchor) - } - - fn fetch_existing_nullifiers( - &self, - _: &[BlsScalar], - ) -> Result, Self::Error> { - Ok(vec![]) - } - - fn fetch_opening( - &self, - _: &Note, - ) -> Result, Self::Error> { - Ok(self.opening) - } - - fn fetch_stake(&self, _pk: &PublicKey) -> Result { - Ok(StakeInfo { - amount: Some((100, 0)), - reward: 0, - counter: 0, - }) - } -} - -#[derive(Debug)] -pub struct TestProverClient; - -impl ProverClient for TestProverClient { - type Error = (); - fn compute_proof_and_propagate( - &self, - utx: &UnprovenTransaction, - ) -> Result { - Ok(utx.clone().prove(Proof::default())) - } - - fn request_stct_proof( - &self, - _fee: &Fee, - _crossover: &Crossover, - _value: u64, - _blinder: JubJubScalar, - _address: BlsScalar, - _signature: Signature, - ) -> Result { - Ok(Proof::default()) - } - - fn request_wfct_proof( - &self, - _commitment: JubJubAffine, - _value: u64, - _blinder: JubJubScalar, - ) -> Result { - Ok(Proof::default()) - } -} - -#[derive(Debug)] -pub struct RkyvProverClient { - prover: TestProverClient, -} - -impl ProverClient for RkyvProverClient { - type Error = (); - - fn compute_proof_and_propagate( - &self, - utx: &UnprovenTransaction, - ) -> Result { - let utx_clone = utx.clone(); - - let tx = utx_clone.prove(Proof::default()); - - let bytes = rkyv::to_bytes::<_, 65536>(&tx) - .expect("Encoding a tx should succeed") - .to_vec(); - - let decoded_tx: Transaction = rkyv::from_bytes(&bytes) - .expect("Deserializing a transaction should succeed"); - - assert_eq!( - tx, decoded_tx, - "Encoded and decoded transaction should be equal" - ); - - self.prover.compute_proof_and_propagate(utx) - } - - fn request_stct_proof( - &self, - fee: &Fee, - crossover: &Crossover, - value: u64, - blinder: JubJubScalar, - address: BlsScalar, - signature: Signature, - ) -> Result { - self.prover.request_stct_proof( - fee, crossover, value, blinder, address, signature, - ) - } - - fn request_wfct_proof( - &self, - commitment: JubJubAffine, - value: u64, - blinder: JubJubScalar, - ) -> Result { - self.prover.request_wfct_proof(commitment, value, blinder) - } -} - -#[derive(Debug)] -pub struct SerdeProverClient { - prover: TestProverClient, -} - -impl ProverClient for SerdeProverClient { - type Error = (); - - fn compute_proof_and_propagate( - &self, - utx: &UnprovenTransaction, - ) -> Result { - let utx_bytes = utx.to_var_bytes(); - let utx_clone = UnprovenTransaction::from_slice(&utx_bytes) - .expect("Successful deserialization"); - - for (input, cinput) in - utx.inputs().iter().zip(utx_clone.inputs().iter()) - { - assert_eq!(input.nullifier(), cinput.nullifier()); - // assert_eq!(input.opening(), cinput.opening()); - assert_eq!(input.note(), cinput.note()); - assert_eq!(input.value(), cinput.value()); - assert_eq!(input.blinding_factor(), cinput.blinding_factor()); - assert_eq!(input.pk_r_prime(), cinput.pk_r_prime()); - // assert_eq!(input.signature(), cinput.signature()); - } - - for (output, coutput) in - utx.outputs().iter().zip(utx_clone.outputs().iter()) - { - assert_eq!(output, coutput); - } - - assert_eq!(utx.anchor(), utx_clone.anchor()); - assert_eq!(utx.fee(), utx_clone.fee()); - assert_eq!(utx.crossover(), utx_clone.crossover()); - assert_eq!(utx.call(), utx_clone.call()); - - self.prover.compute_proof_and_propagate(utx) - } - - fn request_stct_proof( - &self, - fee: &Fee, - crossover: &Crossover, - value: u64, - blinder: JubJubScalar, - address: BlsScalar, - signature: Signature, - ) -> Result { - self.prover.request_stct_proof( - fee, crossover, value, blinder, address, signature, - ) - } - - fn request_wfct_proof( - &self, - commitment: JubJubAffine, - value: u64, - blinder: JubJubScalar, - ) -> Result { - self.prover.request_wfct_proof(commitment, value, blinder) - } -} diff --git a/tests/wallet.rs b/tests/wallet.rs index 55ef628..de715a0 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -6,82 +6,302 @@ //! Wallet library tests. -mod mock; - -use dusk_bytes::Serializable; -use dusk_plonk::prelude::BlsScalar; -use dusk_wallet_core::StakeInfo; -use mock::{mock_canon_wallet, mock_serde_wallet, mock_wallet}; +use dusk_wallet_core::{ + tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, + MAX_LEN, +}; +use std::collections::HashMap; +use wasmer::{imports, Function, Instance, Memory, Module, Store, Value}; #[test] -fn serde_stake() { - let stake = StakeInfo { - amount: Some((1000, 0)), - reward: 100, - counter: 1, - }; +fn balance_works() { + let seed = [0xfa; utils::RNG_SEED]; + let values = [10, 250, 15, 39]; + + let notes = node::notes(&seed, values); + + let args = BalanceArgs { seed, notes }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); + + let mut wallet = Wallet::default(); - let stake_bytes = stake.to_bytes(); - let des_stake = - StakeInfo::from_bytes(&stake_bytes).expect("serde to go correctly"); + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - assert_eq!(stake.amount, des_stake.amount); - assert_eq!(stake.reward, des_stake.reward); - assert_eq!(stake.counter, des_stake.counter); + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("balance", &[ptr, len])[0].unwrap_i32() as u64; + let balance = wallet.memory_read(ptr, BalanceResponse::LEN); + let balance = rkyv::from_bytes::(&balance) + .expect("failed to deserialize balance"); + + let ptr = Value::I32(ptr as i32); + let len = Value::I32(BalanceResponse::LEN as i32); + wallet.call("free_mem", &[ptr, len]); + + assert!(balance.success); + assert_eq!(balance.value, values.into_iter().sum::()) } #[test] -fn serde() { - let mut rng = rand::thread_rng(); +fn execute_works() { + let seed = [0xfa; utils::RNG_SEED]; + let rng_seed = [0xfb; utils::RNG_SEED]; + let values = [10, 250, 15, 7500]; + + let (inputs, openings) = node::notes_and_openings(&seed, values); + let refund = node::psk(&seed); + let output = node::output(&seed, 133); + let crossover = 35; + let gas_limit = 100; + let gas_price = 2; + let call = node::empty_call_data(); + let args = ExecuteArgs { + seed, + rng_seed, + inputs, + openings, + refund, + output, + crossover, + gas_limit, + gas_price, + call, + }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); - let wallet = mock_serde_wallet(&mut rng, &[2500, 2500, 5000]); + let mut wallet = Wallet::default(); - let send_psk = wallet.public_spend_key(0).unwrap(); - let recv_psk = wallet.public_spend_key(1).unwrap(); + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - let ref_id = BlsScalar::random(&mut rng); - wallet - .transfer(&mut rng, 0, &send_psk, &recv_psk, 100, 100, 1, ref_id) - .expect("Transaction creation to be successful"); + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("execute", &[ptr, len])[0].unwrap_i32() as u64; + let execute = wallet.memory_read(ptr, ExecuteResponse::LEN); + let execute = rkyv::from_bytes::(&execute) + .expect("failed to deserialize execute"); + + let ptr = Value::I32(ptr as i32); + let len = Value::I32(ExecuteResponse::LEN as i32); + wallet.call("free_mem", &[ptr, len]); + + let unspent = + wallet.memory_read(execute.unspent_ptr, execute.unspent_len as usize); + let _unspent: Vec = + rkyv::from_bytes(&unspent).expect("failed to deserialize notes"); + + let ptr = Value::I32(execute.unspent_ptr as i32); + let len = Value::I32(execute.unspent_len as i32); + wallet.call("free_mem", &[ptr, len]); + + let tx = wallet.memory_read(execute.tx_ptr, execute.tx_len as usize); + let _tx: tx::UnprovenTransaction = + rkyv::from_bytes(&tx).expect("failed to deserialize tx"); + + let ptr = Value::I32(execute.tx_ptr as i32); + let len = Value::I32(execute.tx_len as i32); + wallet.call("free_mem", &[ptr, len]); + + assert!(execute.success); } -#[test] -fn canon() { - let mut rng = rand::thread_rng(); +/// A node interface. It will encapsulate all the phoenix core functionality. +mod node { + use core::mem; + + use dusk_jubjub::JubJubScalar; + use dusk_wallet_core::{key, tx, utils, MAX_KEY, MAX_LEN}; + use phoenix_core::{Note, NoteType}; + use rand::RngCore; - let wallet = mock_canon_wallet(&mut rng, &[2500, 2500, 5000]); + pub fn notes( + seed: &[u8; utils::RNG_SEED], + values: Values, + ) -> Vec + where + Values: IntoIterator, + { + let rng = &mut utils::rng(seed); + let notes: Vec<_> = values + .into_iter() + .map(|value| { + let obfuscated = (rng.next_u32() & 1) == 1; + let idx = rng.next_u64() % MAX_KEY as u64; + let psk = key::derive_ssk(seed, idx).public_spend_key(); - let send_psk = wallet.public_spend_key(0).unwrap(); - let recv_psk = wallet.public_spend_key(1).unwrap(); + if obfuscated { + let blinder = JubJubScalar::random(rng); + Note::obfuscated(rng, &psk, value, blinder) + } else { + Note::transparent(rng, &psk, value) + } + }) + .collect(); - let ref_id = BlsScalar::random(&mut rng); - wallet - .transfer(&mut rng, 0, &send_psk, &recv_psk, 100, 100, 1, ref_id) - .expect("Transaction creation to be successful"); + rkyv::to_bytes::<_, MAX_LEN>(¬es) + .expect("failed to serialize notes") + .into_vec() + } + + pub fn notes_and_openings( + seed: &[u8; utils::RNG_SEED], + values: Values, + ) -> (Vec, Vec) + where + Values: IntoIterator, + { + let rng = &mut utils::rng(seed); + let notes: Vec<_> = values + .into_iter() + .map(|value| { + let obfuscated = (rng.next_u32() & 1) == 1; + let idx = rng.next_u64() % MAX_KEY as u64; + let psk = key::derive_ssk(seed, idx).public_spend_key(); + + if obfuscated { + let blinder = JubJubScalar::random(rng); + Note::obfuscated(rng, &psk, value, blinder) + } else { + Note::transparent(rng, &psk, value) + } + }) + .collect(); + + let openings: Vec<_> = (0..notes.len()) + .map(|_| unsafe { mem::zeroed::() }) + .collect(); + + let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es) + .expect("failed to serialize notes") + .into_vec(); + + let openings = rkyv::to_bytes::<_, MAX_LEN>(&openings) + .expect("failed to serialize openings") + .into_vec(); + + (notes, openings) + } + + pub fn psk(seed: &[u8; utils::RNG_SEED]) -> Vec { + let psk = key::derive_ssk(seed, 0).public_spend_key(); + rkyv::to_bytes::<_, MAX_LEN>(&psk) + .expect("failed to serialize psk") + .into_vec() + } + + pub fn output(seed: &[u8; utils::RNG_SEED], value: u64) -> Vec { + let rng = &mut utils::rng(seed); + let obfuscated = (rng.next_u32() & 1) == 1; + let r#type = if obfuscated { + NoteType::Obfuscated + } else { + NoteType::Transparent + }; + let receiver = key::derive_ssk(seed, 1).public_spend_key(); + let ref_id = rng.next_u64(); + let output = Some(tx::OutputValue { + r#type, + value, + receiver, + ref_id, + }); + + rkyv::to_bytes::<_, MAX_LEN>(&output) + .expect("failed to serialize notes") + .into_vec() + } + + pub fn empty_call_data() -> Vec { + let call: Option = None; + rkyv::to_bytes::<_, MAX_LEN>(&call) + .expect("failed to serialize call data") + .into_vec() + } } -#[test] -fn transfer() { - let mut rng = rand::thread_rng(); +pub struct Wallet { + pub store: Store, + pub module: Module, + pub memory: Memory, + pub f: HashMap<&'static str, Function>, +} - let wallet = mock_wallet(&mut rng, &[2500, 2500, 5000]); +impl Wallet { + pub fn call(&mut self, f: &str, args: &[Value]) -> Box<[Value]> { + self.f[f] + .call(&mut self.store, args) + .expect("failed to call module function") + } - let send_psk = wallet.public_spend_key(0).unwrap(); - let recv_psk = wallet.public_spend_key(1).unwrap(); + pub fn memory_write(&mut self, ptr: u64, data: &[u8]) { + self.memory + .view(&self.store) + .write(ptr, data) + .expect("failed to write memory"); + } - let ref_id = BlsScalar::random(&mut rng); - wallet - .transfer(&mut rng, 0, &send_psk, &recv_psk, 100, 100, 1, ref_id) - .expect("Transaction creation to be successful"); + pub fn memory_read(&self, ptr: u64, len: usize) -> Vec { + let mut bytes = vec![0u8; len]; + self.memory + .view(&self.store) + .read(ptr, &mut bytes) + .expect("failed to read memory"); + bytes + } } -#[test] -fn get_balance() { - let mut rng = rand::thread_rng(); +impl Default for Wallet { + fn default() -> Self { + const WALLET: &[u8] = include_bytes!( + "../target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm" + ); + + let mut store = Store::default(); + let module = + Module::new(&store, WALLET).expect("failed to create wasm module"); + + let import_object = imports! {}; + let instance = Instance::new(&mut store, &module, &import_object) + .expect("failed to instanciate the wasm module"); + + let memory = instance + .exports + .get_memory("memory") + .expect("failed to get instance memory") + .clone(); + + fn add_function( + map: &mut HashMap<&'static str, Function>, + instance: &Instance, + name: &'static str, + ) { + map.insert( + name, + instance + .exports + .get_function(name) + .expect("failed to import wasm function") + .clone(), + ); + } + + let mut f = HashMap::new(); - let wallet = mock_wallet(&mut rng, &[2500, 5000, 2500, 5000, 5000]); - let info = wallet.get_balance(0).expect("Valid balance call"); + add_function(&mut f, &instance, "malloc"); + add_function(&mut f, &instance, "free_mem"); + add_function(&mut f, &instance, "balance"); + add_function(&mut f, &instance, "execute"); - assert_eq!(info.value, 20000); - assert_eq!(info.spendable, 17500); + Self { + store, + module, + memory, + f, + } + } } From 61487fc9fd3bcd76ee13bd160a0a0a1d77b5b836 Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 03:32:50 +0200 Subject: [PATCH 02/29] fix CI --- .github/workflows/dusk_ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index a4fb766..d6e88f6 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -62,6 +62,8 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} + - run: make wasm + - name: Run cargo check uses: actions-rs/cargo@v1 with: @@ -118,14 +120,14 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} + - run: make wasm + - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check args: --all-targets - - run: make wasm - - name: Test project if: ${{ matrix.os != 'ubuntu-latest' || matrix.toolchain != 'nightly' }} uses: actions-rs/cargo@v1 From 7b7e3e755ef9338071b5a1fbe5dcdd34d500ea55 Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 03:41:28 +0200 Subject: [PATCH 03/29] add maximum value to balance --- .github/workflows/dusk_ci.yml | 3 +++ src/ffi.rs | 13 +++++++++++-- src/lib.rs | 2 ++ tests/wallet.rs | 5 +++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index d6e88f6..27f2f01 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -120,6 +120,9 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} + - name: Add WASM target + run: rustup target add wasm32-unknown-unknown + - run: make wasm - name: Run cargo check diff --git a/src/ffi.rs b/src/ffi.rs index c756bc3..03b7173 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -58,6 +58,7 @@ pub fn balance(args: i32, len: i32) -> i32 { }; let mut keys = unsafe { [mem::zeroed(); MAX_KEY] }; + let mut values = Vec::with_capacity(notes.len()); let mut keys_len = 0; let mut sum = 0u64; @@ -71,6 +72,7 @@ pub fn balance(args: i32, len: i32) -> i32 { } if let Ok(v) = note.value(Some(&keys[idx])) { + values.push(v); sum = sum.saturating_add(v); continue 'outer; } @@ -79,7 +81,12 @@ pub fn balance(args: i32, len: i32) -> i32 { return BalanceResponse::fail(); } - BalanceResponse::success(sum) + // the top 4 notes are the maximum value a transaction can have, given the + // circuit accepts up to 4 inputs + values.sort_by(|a, b| b.cmp(a)); + let maximum = values.iter().take(4).sum::(); + + BalanceResponse::success(sum, maximum) } /// Computes a serialized unproven transaction from the given arguments. @@ -218,10 +225,11 @@ impl BalanceResponse { /// Returns a representation of a successful balance operation with the /// computed value. - pub fn success(value: u64) -> i32 { + pub fn success(value: u64, maximum: u64) -> i32 { Self { success: true, value, + maximum, } .as_i32_ptr() } @@ -231,6 +239,7 @@ impl BalanceResponse { Self { success: false, value: 0, + maximum: 0, } .as_i32_ptr() } diff --git a/src/lib.rs b/src/lib.rs index 108abf8..afe47b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ pub struct BalanceResponse { pub success: bool, /// Total computed balance pub value: u64, + /// Maximum value per transaction + pub maximum: u64, } impl BalanceResponse { diff --git a/tests/wallet.rs b/tests/wallet.rs index de715a0..7d789ee 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -16,7 +16,7 @@ use wasmer::{imports, Function, Instance, Memory, Module, Store, Value}; #[test] fn balance_works() { let seed = [0xfa; utils::RNG_SEED]; - let values = [10, 250, 15, 39]; + let values = [10, 250, 15, 39, 55]; let notes = node::notes(&seed, values); @@ -42,7 +42,8 @@ fn balance_works() { wallet.call("free_mem", &[ptr, len]); assert!(balance.success); - assert_eq!(balance.value, values.into_iter().sum::()) + assert_eq!(balance.value, values.into_iter().sum::()); + assert_eq!(balance.maximum, 359); } #[test] From 8484753891f40a24b86f5b59dd8e3ebfa9d3fbfe Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 04:15:28 +0200 Subject: [PATCH 04/29] fix CI --- .github/workflows/dusk_ci.yml | 11 +++++++---- Makefile | 3 +-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 27f2f01..366290e 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -9,6 +9,7 @@ on: name: Dusk CI jobs: + analyze: name: Dusk Analyzer runs-on: ubuntu-latest @@ -40,6 +41,7 @@ jobs: command: fmt args: --all -- --check + build_wasm: name: Build WASM strategy: @@ -62,6 +64,8 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} + - run: rustup target add wasm32-unknown-unknown + - run: rustup component add rust-src - run: make wasm - name: Run cargo check @@ -100,6 +104,7 @@ jobs: env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build_and_test: name: Test with all features strategy: @@ -120,9 +125,8 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} - - name: Add WASM target - run: rustup target add wasm32-unknown-unknown - + - run: rustup target add wasm32-unknown-unknown + - run: rustup component add rust-src - run: make wasm - name: Run cargo check @@ -132,7 +136,6 @@ jobs: args: --all-targets - name: Test project - if: ${{ matrix.os != 'ubuntu-latest' || matrix.toolchain != 'nightly' }} uses: actions-rs/cargo@v1 with: command: test diff --git a/Makefile b/Makefile index 3bab60b..9f3eb19 100644 --- a/Makefile +++ b/Makefile @@ -16,8 +16,7 @@ wasm: ## Build the WASM files --target wasm32-unknown-unknown package: ## Prepare the WASM npm package - wasm-opt -O4 \ - --output-target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ + wasm-opt -O4 target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ -o mod.wasm .PHONY: test wasm help From 060d6eaac76af0882bcb1d14defa6630a173eeac Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 15:18:51 +0200 Subject: [PATCH 05/29] add `merge_notes` and `view_keys` methods --- src/ffi.rs | 175 +++++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 55 ++++++++++++--- src/utils.rs | 7 ++ tests/wallet.rs | 140 ++++++++++++++++++++++++++++++++++++-- 4 files changed, 353 insertions(+), 24 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 03b7173..143819e 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -10,11 +10,14 @@ use alloc::{vec, vec::Vec}; use core::mem; use dusk_pki::{PublicSpendKey, SecretSpendKey}; -use phoenix_core::note::{Note, NoteType}; +use phoenix_core::note::{ArchivedNote, Note, NoteType}; +use rkyv::validation::validators::FromBytesError; use crate::{ - key, tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, - MAX_KEY, MAX_LEN, + key, tx, utils, ArchivedBalanceResponse, ArchivedExecuteResponse, + ArchivedMergeNotesResponse, ArchivedViewKeysResponse, BalanceArgs, + BalanceResponse, ExecuteArgs, ExecuteResponse, MergeNotesArgs, + MergeNotesResponse, ViewKeysArgs, ViewKeysResponse, MAX_KEY, MAX_LEN, }; /// Allocates a buffer of `len` bytes on the WASM memory. @@ -53,11 +56,11 @@ pub fn balance(args: i32, len: i32) -> i32 { }; let notes: Vec = match rkyv::from_bytes(¬es) { - Ok(n) => n, + Ok(n) => utils::sanitize_notes(n), Err(_) => return BalanceResponse::fail(), }; - let mut keys = unsafe { [mem::zeroed(); MAX_KEY] }; + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; let mut values = Vec::with_capacity(notes.len()); let mut keys_len = 0; let mut sum = 0u64; @@ -65,7 +68,7 @@ pub fn balance(args: i32, len: i32) -> i32 { 'outer: for note in notes { // we iterate all the available keys until one can successfully decrypt // the note. if all fails, returns false - for idx in 0..MAX_KEY { + for idx in 0..=MAX_KEY { if keys_len == idx { keys[idx] = key::derive_vk(&seed, idx as u64); keys_len += 1; @@ -119,6 +122,7 @@ pub fn execute(args: i32, len: i32) -> i32 { }: ExecuteArgs, ) -> Option<(Vec, Vec)> { let inputs: Vec = rkyv::from_bytes(&inputs).ok()?; + let inputs = utils::sanitize_notes(inputs); let openings: Vec = rkyv::from_bytes(&openings).ok()?; let refund: PublicSpendKey = rkyv::from_bytes(&refund).ok()?; let output: Option = rkyv::from_bytes(&output).ok()?; @@ -128,9 +132,9 @@ pub fn execute(args: i32, len: i32) -> i32 { let total_output = gas_limit.saturating_mul(gas_price).saturating_add(value); - let mut keys = unsafe { [mem::zeroed(); MAX_KEY] }; + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; let mut keys_ssk = - unsafe { [mem::zeroed::(); MAX_KEY] }; + unsafe { [mem::zeroed::(); MAX_KEY + 1] }; let mut keys_len = 0; let mut openings = openings.into_iter(); let mut full_inputs = Vec::with_capacity(inputs.len()); @@ -138,7 +142,7 @@ pub fn execute(args: i32, len: i32) -> i32 { 'outer: for input in inputs { // we iterate all the available keys until one can successfully // decrypt the note. if any fails, returns false - for idx in 0..MAX_KEY { + for idx in 0..=MAX_KEY { if keys_len == idx { keys_ssk[idx] = key::derive_ssk(&seed, idx as u64); keys[idx] = keys_ssk[idx].view_key(); @@ -210,7 +214,83 @@ pub fn execute(args: i32, len: i32) -> i32 { ExecuteResponse::success(unspent_ptr, unspent_len, tx_ptr, tx_len) } +/// Merges many lists of serialized notes into a unique, sanitized set. +/// +/// The arguments are expected to be rkyv serialized [MergeNotesArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn merge_notes(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + + let MergeNotesArgs { notes } = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return MergeNotesResponse::fail(), + }; + + let len = 3 * notes.len() / mem::size_of::() / 2; + let notes = match notes + .into_iter() + .map(|n| rkyv::from_bytes::>(&n)) + .try_fold::<_, _, Result<_, FromBytesError>>>( + Vec::with_capacity(len), + |mut set, notes| { + set.extend(notes?); + Ok(utils::sanitize_notes(set)) + }, + ) { + Ok(n) => n, + Err(_) => return MergeNotesResponse::fail(), + }; + + let notes = match rkyv::to_bytes::<_, MAX_LEN>(¬es) { + Ok(n) => n.into_vec(), + Err(_) => return MergeNotesResponse::fail(), + }; + + let notes_ptr = notes.as_ptr() as u64; + let notes_len = notes.len() as u64; + + MergeNotesResponse::success(notes_ptr, notes_len) +} + +/// Returns a list of [ViewKey] that belongs to this wallet. +/// +/// The arguments are expected to be rkyv serialized [ViewKeysArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn view_keys(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + + let ViewKeysArgs { seed } = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return ViewKeysResponse::fail(), + }; + + let vks: Vec<_> = (0..=MAX_KEY) + .map(|idx| key::derive_vk(&seed, idx as u64)) + .collect(); + + let vks = match rkyv::to_bytes::<_, MAX_LEN>(&vks) { + Ok(k) => k.into_vec(), + Err(_) => return ViewKeysResponse::fail(), + }; + + let vks_ptr = vks.as_ptr() as u64; + let vks_len = vks.len() as u64; + + ViewKeysResponse::success(vks_ptr, vks_len) +} + impl BalanceResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + fn as_i32_ptr(&self) -> i32 { let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { Ok(b) => b.into_vec(), @@ -246,6 +326,9 @@ impl BalanceResponse { } impl ExecuteResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + fn as_i32_ptr(&self) -> i32 { let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { Ok(b) => b.into_vec(), @@ -288,3 +371,77 @@ impl ExecuteResponse { .as_i32_ptr() } } + +impl MergeNotesResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; + + let ptr = b.as_ptr() as i32; + mem::forget(b); + + ptr + } + + /// Returns a representation of a successful merge_notes operation. + pub fn success(notes_ptr: u64, notes_len: u64) -> i32 { + Self { + success: true, + notes_ptr, + notes_len, + } + .as_i32_ptr() + } + + /// Returns a representation of the failure of the merge_notes operation. + pub fn fail() -> i32 { + Self { + success: false, + notes_ptr: 0, + notes_len: 0, + } + .as_i32_ptr() + } +} + +impl ViewKeysResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; + + let ptr = b.as_ptr() as i32; + mem::forget(b); + + ptr + } + + /// Returns a representation of a successful view_keys operation. + pub fn success(vks_ptr: u64, vks_len: u64) -> i32 { + Self { + success: true, + vks_ptr, + vks_len, + } + .as_i32_ptr() + } + + /// Returns a representation of the failure of the view_keys operation. + pub fn fail() -> i32 { + Self { + success: false, + vks_ptr: 0, + vks_len: 0, + } + .as_i32_ptr() + } +} diff --git a/src/lib.rs b/src/lib.rs index afe47b7..89bc174 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,6 @@ extern crate alloc; use alloc::vec::Vec; -use core::mem; use bytecheck::CheckBytes; use rkyv::{Archive, Deserialize, Serialize}; @@ -21,7 +20,8 @@ pub mod key; pub mod tx; pub mod utils; -/// The maximum number of keys to derive when attempting to decrypt a note. +/// The maximum number of keys (inclusive) to derive when attempting to decrypt +/// a note. pub const MAX_KEY: usize = 24; /// The maximum allocated buffer for rkyv serialization. @@ -52,11 +52,6 @@ pub struct BalanceResponse { pub maximum: u64, } -impl BalanceResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); -} - /// The arguments of the execute function. #[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] #[archive_attr(derive(CheckBytes))] @@ -104,7 +99,47 @@ pub struct ExecuteResponse { pub tx_len: u64, } -impl ExecuteResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); +/// The arguments of the merge_notes function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct MergeNotesArgs { + /// All serialized list of notes to be merged. + pub notes: Vec>, +} + +/// The response of the merge_notes function. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, +)] +#[archive_attr(derive(CheckBytes))] +pub struct MergeNotesResponse { + /// Status of the execution + pub success: bool, + /// The pointer to a rkyv serialized [Vec>] + /// containing the merged notes set. + pub notes_ptr: u64, + /// The length of the rkyv serialized `notes_ptr`. + pub notes_len: u64, +} + +/// The arguments of the view_keys function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct ViewKeysArgs { + /// Seed used to derive the keys of the wallet. + pub seed: [u8; utils::RNG_SEED], +} + +/// The response of the view_keys function. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, +)] +#[archive_attr(derive(CheckBytes))] +pub struct ViewKeysResponse { + /// Status of the execution + pub success: bool, + /// The pointer to a rkyv serialized [Vec>]. + pub vks_ptr: u64, + /// The length of the rkyv serialized `vks_ptr`. + pub vks_len: u64, } diff --git a/src/utils.rs b/src/utils.rs index 5b67f2b..6383cf8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -41,6 +41,13 @@ pub fn rng_with_index(seed: &[u8; RNG_SEED], index: u64) -> ChaCha12Rng { ChaCha12Rng::from_seed(hash) } +/// Sanitize a notes input into a consumable notes set +pub fn sanitize_notes(mut notes: Vec) -> Vec { + notes.sort_by_key(|n| n.hash()); + notes.dedup(); + notes +} + /// Perform a knapsack algorithm to define the notes to be used as input. /// /// Returns a tuple containing (unspent, inputs). `unspent` contains the notes diff --git a/tests/wallet.rs b/tests/wallet.rs index 7d789ee..e0c6893 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -8,6 +8,7 @@ use dusk_wallet_core::{ tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, + MergeNotesArgs, MergeNotesResponse, ViewKeysArgs, ViewKeysResponse, MAX_LEN, }; use std::collections::HashMap; @@ -111,6 +112,125 @@ fn execute_works() { assert!(execute.success); } +#[test] +fn merge_notes_works() { + let seed = [0xfa; utils::RNG_SEED]; + + let notes1 = node::raw_notes(&seed, [10, 250, 15, 39, 55]); + let notes2 = vec![notes1[1].clone(), notes1[3].clone()]; + let notes3: Vec<_> = node::raw_notes(&seed, [10, 250, 15, 39, 55]) + .into_iter() + .chain([notes1[4].clone()]) + .collect(); + + let notes_unmerged: Vec<_> = notes1 + .iter() + .chain(notes2.iter()) + .chain(notes3.iter()) + .cloned() + .collect(); + + let mut notes_merged = notes_unmerged.clone(); + notes_merged.sort_by_key(|n| n.hash()); + notes_merged.dedup(); + + assert_ne!(notes_unmerged, notes_merged); + + let notes1 = rkyv::to_bytes::<_, MAX_LEN>(¬es1).unwrap().into_vec(); + let notes2 = rkyv::to_bytes::<_, MAX_LEN>(¬es2).unwrap().into_vec(); + let notes3 = rkyv::to_bytes::<_, MAX_LEN>(¬es3).unwrap().into_vec(); + let notes = vec![notes1, notes2, notes3]; + + let args = MergeNotesArgs { notes }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); + + let mut wallet = Wallet::default(); + + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("merge_notes", &[ptr, len])[0].unwrap_i32() as u64; + + let notes = wallet.memory_read(ptr, MergeNotesResponse::LEN); + let notes = rkyv::from_bytes::(¬es) + .expect("failed to deserialize merged notes"); + + let ptr = Value::I32(ptr as i32); + let len = Value::I32(MergeNotesResponse::LEN as i32); + wallet.call("free_mem", &[ptr, len]); + + let merged = wallet.memory_read(notes.notes_ptr, notes.notes_len as usize); + let merged: Vec = + rkyv::from_bytes(&merged).expect("failed to deserialize notes"); + + assert!(notes.success); + assert_eq!(merged, notes_merged); +} + +#[test] +fn view_keys_works() { + let seed = [0xfa; utils::RNG_SEED]; + + let args = ViewKeysArgs { seed }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); + + let keys = { + let mut wallet = Wallet::default(); + + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("view_keys", &[ptr, len])[0].unwrap_i32() as u64; + + let response = wallet.memory_read(ptr, ViewKeysResponse::LEN); + let response = rkyv::from_bytes::(&response) + .expect("failed to deserialize view keys"); + + assert!(response.success); + + let keys = + wallet.memory_read(response.vks_ptr, response.vks_len as usize); + let keys: Vec = + rkyv::from_bytes(&keys).expect("failed to deserialize keys"); + keys + }; + + let keys_p = { + let mut wallet = Wallet::default(); + + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("view_keys", &[ptr, len])[0].unwrap_i32() as u64; + + let response = wallet.memory_read(ptr, ViewKeysResponse::LEN); + let response = rkyv::from_bytes::(&response) + .expect("failed to deserialize view keys"); + + assert!(response.success); + + let keys = + wallet.memory_read(response.vks_ptr, response.vks_len as usize); + let keys: Vec = + rkyv::from_bytes(&keys).expect("failed to deserialize keys"); + keys + }; + + // assert keys generation is deterministic + assert_eq!(keys, keys_p); +} + /// A node interface. It will encapsulate all the phoenix core functionality. mod node { use core::mem; @@ -120,15 +240,15 @@ mod node { use phoenix_core::{Note, NoteType}; use rand::RngCore; - pub fn notes( + pub fn raw_notes( seed: &[u8; utils::RNG_SEED], values: Values, - ) -> Vec + ) -> Vec where Values: IntoIterator, { let rng = &mut utils::rng(seed); - let notes: Vec<_> = values + values .into_iter() .map(|value| { let obfuscated = (rng.next_u32() & 1) == 1; @@ -142,9 +262,17 @@ mod node { Note::transparent(rng, &psk, value) } }) - .collect(); + .collect() + } - rkyv::to_bytes::<_, MAX_LEN>(¬es) + pub fn notes( + seed: &[u8; utils::RNG_SEED], + values: Values, + ) -> Vec + where + Values: IntoIterator, + { + rkyv::to_bytes::<_, MAX_LEN>(&raw_notes(seed, values)) .expect("failed to serialize notes") .into_vec() } @@ -297,6 +425,8 @@ impl Default for Wallet { add_function(&mut f, &instance, "free_mem"); add_function(&mut f, &instance, "balance"); add_function(&mut f, &instance, "execute"); + add_function(&mut f, &instance, "merge_notes"); + add_function(&mut f, &instance, "view_keys"); Self { store, From f0e236b46ddcc4659577d6ed9ab7ef33687561fb Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 17:30:25 +0200 Subject: [PATCH 06/29] add `filter_notes` and `nullifiers` methods --- src/ffi.rs | 189 ++++++++++++++++++++++++++++++++++++++++++++++-- src/lib.rs | 50 +++++++++++++ tests/wallet.rs | 118 +++++++++++++++++++++++++++++- 3 files changed, 349 insertions(+), 8 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index 143819e..d962af5 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -9,15 +9,17 @@ use alloc::{vec, vec::Vec}; use core::mem; -use dusk_pki::{PublicSpendKey, SecretSpendKey}; +use dusk_pki::PublicSpendKey; use phoenix_core::note::{ArchivedNote, Note, NoteType}; use rkyv::validation::validators::FromBytesError; use crate::{ key, tx, utils, ArchivedBalanceResponse, ArchivedExecuteResponse, - ArchivedMergeNotesResponse, ArchivedViewKeysResponse, BalanceArgs, - BalanceResponse, ExecuteArgs, ExecuteResponse, MergeNotesArgs, - MergeNotesResponse, ViewKeysArgs, ViewKeysResponse, MAX_KEY, MAX_LEN, + ArchivedFilterNotesResponse, ArchivedMergeNotesResponse, + ArchivedNullifiersResponse, ArchivedViewKeysResponse, BalanceArgs, + BalanceResponse, ExecuteArgs, ExecuteResponse, FilterNotesArgs, + FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, NullifiersArgs, + NullifiersResponse, ViewKeysArgs, ViewKeysResponse, MAX_KEY, MAX_LEN, }; /// Allocates a buffer of `len` bytes on the WASM memory. @@ -133,8 +135,7 @@ pub fn execute(args: i32, len: i32) -> i32 { gas_limit.saturating_mul(gas_price).saturating_add(value); let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; - let mut keys_ssk = - unsafe { [mem::zeroed::(); MAX_KEY + 1] }; + let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; let mut keys_len = 0; let mut openings = openings.into_iter(); let mut full_inputs = Vec::with_capacity(inputs.len()); @@ -256,6 +257,51 @@ pub fn merge_notes(args: i32, len: i32) -> i32 { MergeNotesResponse::success(notes_ptr, notes_len) } +/// Filters a list of notes from a list of negative flags. The flags that are +/// `true` will represent a note that must be removed from the set. +/// +/// The arguments are expected to be rkyv serialized [FilterNotesArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn filter_notes(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + + let FilterNotesArgs { notes, flags } = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return FilterNotesResponse::fail(), + }; + + let notes: Vec = match rkyv::from_bytes(¬es) { + Ok(n) => n, + Err(_) => return FilterNotesResponse::fail(), + }; + + let flags: Vec = match rkyv::from_bytes(&flags) { + Ok(f) => f, + Err(_) => return FilterNotesResponse::fail(), + }; + + let notes: Vec<_> = notes + .into_iter() + .zip(flags.into_iter()) + .filter_map(|(n, f)| (!f).then_some(n)) + .collect(); + + let notes = utils::sanitize_notes(notes); + let notes = match rkyv::to_bytes::<_, MAX_LEN>(¬es) { + Ok(n) => n.into_vec(), + Err(_) => return FilterNotesResponse::fail(), + }; + + let notes_ptr = notes.as_ptr() as u64; + let notes_len = notes.len() as u64; + + FilterNotesResponse::success(notes_ptr, notes_len) +} + /// Returns a list of [ViewKey] that belongs to this wallet. /// /// The arguments are expected to be rkyv serialized [ViewKeysArgs] with a @@ -287,6 +333,63 @@ pub fn view_keys(args: i32, len: i32) -> i32 { ViewKeysResponse::success(vks_ptr, vks_len) } +/// Returns a list of [BlsScalar] nullifiers for the given [Vec] combined +/// with the keys of this wallet. +/// +/// The arguments are expected to be rkyv serialized [NullifiersArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn nullifiers(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + + let NullifiersArgs { seed, notes } = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return NullifiersResponse::fail(), + }; + + let notes: Vec = match rkyv::from_bytes(¬es) { + Ok(n) => n, + Err(_) => return NullifiersResponse::fail(), + }; + + let mut nullifiers = Vec::with_capacity(notes.len()); + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_len = 0; + + 'outer: for note in notes { + // we iterate all the available keys until one can successfully + // decrypt the note. if any fails, returns false + for idx in 0..=MAX_KEY { + if keys_len == idx { + keys_ssk[idx] = key::derive_ssk(&seed, idx as u64); + keys[idx] = keys_ssk[idx].view_key(); + keys_len += 1; + } + + if keys[idx].owns(¬e) { + nullifiers.push(note.gen_nullifier(&keys_ssk[idx])); + continue 'outer; + } + } + + return NullifiersResponse::fail(); + } + + let nullifiers = match rkyv::to_bytes::<_, MAX_LEN>(&nullifiers) { + Ok(n) => n.into_vec(), + Err(_) => return NullifiersResponse::fail(), + }; + + let nullifiers_ptr = nullifiers.as_ptr() as u64; + let nullifiers_len = nullifiers.len() as u64; + + NullifiersResponse::success(nullifiers_ptr, nullifiers_len) +} + impl BalanceResponse { /// Rkyv serialized length of the response pub const LEN: usize = mem::size_of::(); @@ -409,6 +512,43 @@ impl MergeNotesResponse { } } +impl FilterNotesResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; + + let ptr = b.as_ptr() as i32; + mem::forget(b); + + ptr + } + + /// Returns a representation of a successful filter_notes operation. + pub fn success(notes_ptr: u64, notes_len: u64) -> i32 { + Self { + success: true, + notes_ptr, + notes_len, + } + .as_i32_ptr() + } + + /// Returns a representation of the failure of the filter_notes operation. + pub fn fail() -> i32 { + Self { + success: false, + notes_ptr: 0, + notes_len: 0, + } + .as_i32_ptr() + } +} + impl ViewKeysResponse { /// Rkyv serialized length of the response pub const LEN: usize = mem::size_of::(); @@ -445,3 +585,40 @@ impl ViewKeysResponse { .as_i32_ptr() } } + +impl NullifiersResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; + + let ptr = b.as_ptr() as i32; + mem::forget(b); + + ptr + } + + /// Returns a representation of a successful nullifiers operation. + pub fn success(nullifiers_ptr: u64, nullifiers_len: u64) -> i32 { + Self { + success: true, + nullifiers_ptr, + nullifiers_len, + } + .as_i32_ptr() + } + + /// Returns a representation of the failure of the nullifiers operation. + pub fn fail() -> i32 { + Self { + success: false, + nullifiers_ptr: 0, + nullifiers_len: 0, + } + .as_i32_ptr() + } +} diff --git a/src/lib.rs b/src/lib.rs index 89bc174..c759197 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,6 +122,31 @@ pub struct MergeNotesResponse { pub notes_len: u64, } +/// The arguments of the filter_notes function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct FilterNotesArgs { + /// Rkyv serialized notes to be filtered. + pub notes: Vec, + /// Rkyv serialized boolean flags to be negative filtered. + pub flags: Vec, +} + +/// The response of the filter_notes function. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, +)] +#[archive_attr(derive(CheckBytes))] +pub struct FilterNotesResponse { + /// Status of the execution + pub success: bool, + /// The pointer to a rkyv serialized [Vec>] + /// containing the filtered notes set. + pub notes_ptr: u64, + /// The length of the rkyv serialized `notes_ptr`. + pub notes_len: u64, +} + /// The arguments of the view_keys function. #[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] #[archive_attr(derive(CheckBytes))] @@ -143,3 +168,28 @@ pub struct ViewKeysResponse { /// The length of the rkyv serialized `vks_ptr`. pub vks_len: u64, } + +/// The arguments of the nullifiers function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct NullifiersArgs { + /// Seed used to derive the keys of the wallet. + pub seed: [u8; utils::RNG_SEED], + /// Rkyv serialized notes to have nullifiers generated. + pub notes: Vec, +} + +/// The response of the view_keys function. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, +)] +#[archive_attr(derive(CheckBytes))] +pub struct NullifiersResponse { + /// Status of the execution + pub success: bool, + /// The pointer to a rkyv serialized [Vec>] containing the + /// nullifiers ordered list. + pub nullifiers_ptr: u64, + /// The length of the rkyv serialized `nullifiers_ptr`. + pub nullifiers_len: u64, +} diff --git a/tests/wallet.rs b/tests/wallet.rs index e0c6893..e98a86e 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -8,7 +8,8 @@ use dusk_wallet_core::{ tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, - MergeNotesArgs, MergeNotesResponse, ViewKeysArgs, ViewKeysResponse, + FilterNotesArgs, FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, + NullifiersArgs, NullifiersResponse, ViewKeysArgs, ViewKeysResponse, MAX_LEN, }; use std::collections::HashMap; @@ -171,6 +172,50 @@ fn merge_notes_works() { assert_eq!(merged, notes_merged); } +#[test] +fn filter_notes_works() { + let seed = [0xfa; utils::RNG_SEED]; + + let notes = node::raw_notes(&seed, [10, 250, 15, 39, 55]); + let flags = vec![true, true, false, true, false]; + let filtered = vec![notes[2].clone(), notes[4].clone()]; + let filtered = utils::sanitize_notes(filtered); + + let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es).unwrap().into_vec(); + let flags = rkyv::to_bytes::<_, MAX_LEN>(&flags).unwrap().into_vec(); + + let args = FilterNotesArgs { notes, flags }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); + + let mut wallet = Wallet::default(); + + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("filter_notes", &[ptr, len])[0].unwrap_i32() as u64; + + let response = wallet.memory_read(ptr, FilterNotesResponse::LEN); + let response = rkyv::from_bytes::(&response) + .expect("failed to deserialize filtered notes"); + + assert!(response.success); + + let notes = + wallet.memory_read(response.notes_ptr, response.notes_len as usize); + let notes: Vec = + rkyv::from_bytes(¬es).expect("failed to deserialize notes"); + + let ptr = Value::I32(ptr as i32); + let len = Value::I32(FilterNotesResponse::LEN as i32); + wallet.call("free_mem", &[ptr, len]); + + assert_eq!(notes, filtered); +} + #[test] fn view_keys_works() { let seed = [0xfa; utils::RNG_SEED]; @@ -231,11 +276,49 @@ fn view_keys_works() { assert_eq!(keys, keys_p); } +#[test] +fn nullifiers_works() { + let seed = [0xfa; utils::RNG_SEED]; + + let (notes, nullifiers): (Vec<_>, Vec<_>) = + node::raw_notes_and_nulifiers(&seed, [10, 250, 15, 39, 55]) + .into_iter() + .unzip(); + + let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es).unwrap().into_vec(); + let args = NullifiersArgs { seed, notes }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); + + let mut wallet = Wallet::default(); + + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("nullifiers", &[ptr, len])[0].unwrap_i32() as u64; + + let response = wallet.memory_read(ptr, NullifiersResponse::LEN); + let response = rkyv::from_bytes::(&response) + .expect("failed to deserialize nullifiers"); + + assert!(response.success); + + let response = wallet + .memory_read(response.nullifiers_ptr, response.nullifiers_len as usize); + let response: Vec = + rkyv::from_bytes(&response).expect("failed to deserialize nullifiers"); + + assert_eq!(nullifiers, response); +} + /// A node interface. It will encapsulate all the phoenix core functionality. mod node { use core::mem; - use dusk_jubjub::JubJubScalar; + use dusk_jubjub::{BlsScalar, JubJubScalar}; use dusk_wallet_core::{key, tx, utils, MAX_KEY, MAX_LEN}; use phoenix_core::{Note, NoteType}; use rand::RngCore; @@ -265,6 +348,35 @@ mod node { .collect() } + pub fn raw_notes_and_nulifiers( + seed: &[u8; utils::RNG_SEED], + values: Values, + ) -> Vec<(Note, BlsScalar)> + where + Values: IntoIterator, + { + let rng = &mut utils::rng(seed); + values + .into_iter() + .map(|value| { + let obfuscated = (rng.next_u32() & 1) == 1; + let idx = rng.next_u64() % MAX_KEY as u64; + let ssk = key::derive_ssk(seed, idx); + let psk = ssk.public_spend_key(); + + let note = if obfuscated { + let blinder = JubJubScalar::random(rng); + Note::obfuscated(rng, &psk, value, blinder) + } else { + Note::transparent(rng, &psk, value) + }; + + let nullifier = note.gen_nullifier(&ssk); + (note, nullifier) + }) + .collect() + } + pub fn notes( seed: &[u8; utils::RNG_SEED], values: Values, @@ -426,7 +538,9 @@ impl Default for Wallet { add_function(&mut f, &instance, "balance"); add_function(&mut f, &instance, "execute"); add_function(&mut f, &instance, "merge_notes"); + add_function(&mut f, &instance, "filter_notes"); add_function(&mut f, &instance, "view_keys"); + add_function(&mut f, &instance, "nullifiers"); Self { store, From 7a5c92503c91ed212859110c88d605866bda7b8c Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 17:36:22 +0200 Subject: [PATCH 07/29] remove failing kcov --- .github/workflows/dusk_ci.yml | 54 +++++++++++++++++------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 366290e..014c6d4 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -140,30 +140,30 @@ jobs: with: command: test - - name: Install kcov - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} - run: sudo apt install -y kcov - - - name: Build test executable - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} - uses: actions-rs/cargo@v1 - env: - RUSTFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' - RUSTDOCFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' - with: - command: test - args: --no-run - - - name: Test with kcov - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} - # Find every executable resulting from building the tests and run each - # one of them with kcov. This ensures all the code we cover is measured. - run: > - find target/debug/deps -type f -executable ! -name "*.*" | - xargs -n1 kcov --exclude-pattern=tests/,/.cargo,/usr/lib --verify target/cov - - - name: Upload coverage - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} - uses: codecov/codecov-action@v1.0.2 - with: - token: ${{secrets.CODECOV_TOKEN}} +# - name: Install kcov +# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} +# run: sudo apt install -y kcov +# +# - name: Build test executable +# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} +# uses: actions-rs/cargo@v1 +# env: +# RUSTFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' +# RUSTDOCFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' +# with: +# command: test +# args: --no-run +# +# - name: Test with kcov +# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} +# # Find every executable resulting from building the tests and run each +# # one of them with kcov. This ensures all the code we cover is measured. +# run: > +# find target/debug/deps -type f -executable ! -name "*.*" | +# xargs -n1 kcov --exclude-pattern=tests/,/.cargo,/usr/lib --verify target/cov +# +# - name: Upload coverage +# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} +# uses: codecov/codecov-action@v1.0.2 +# with: +# token: ${{secrets.CODECOV_TOKEN}} From 6ae98df44fc062d41db1454fb5e24f1d4d5faba5 Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 19:44:06 +0200 Subject: [PATCH 08/29] add `seed` method --- src/ffi.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 20 +++++++++++++ tests/wallet.rs | 37 ++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/src/ffi.rs b/src/ffi.rs index d962af5..f715895 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -12,14 +12,16 @@ use core::mem; use dusk_pki::PublicSpendKey; use phoenix_core::note::{ArchivedNote, Note, NoteType}; use rkyv::validation::validators::FromBytesError; +use sha2::{Digest, Sha512}; use crate::{ key, tx, utils, ArchivedBalanceResponse, ArchivedExecuteResponse, ArchivedFilterNotesResponse, ArchivedMergeNotesResponse, - ArchivedNullifiersResponse, ArchivedViewKeysResponse, BalanceArgs, - BalanceResponse, ExecuteArgs, ExecuteResponse, FilterNotesArgs, - FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, NullifiersArgs, - NullifiersResponse, ViewKeysArgs, ViewKeysResponse, MAX_KEY, MAX_LEN, + ArchivedNullifiersResponse, ArchivedSeedResponse, ArchivedViewKeysResponse, + BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, + FilterNotesArgs, FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, + NullifiersArgs, NullifiersResponse, SeedArgs, SeedResponse, ViewKeysArgs, + ViewKeysResponse, MAX_KEY, MAX_LEN, }; /// Allocates a buffer of `len` bytes on the WASM memory. @@ -41,6 +43,36 @@ pub fn free_mem(ptr: i32, len: i32) { } } +/// Computes a secure seed from the given passphrase. +/// +/// The arguments are expected to be rkyv serialized [SeedArgs] with a +/// pointer defined via [malloc]. It will consume the `args` allocated region +/// and drop it. +#[no_mangle] +pub fn seed(args: i32, len: i32) -> i32 { + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + + let SeedArgs { passphrase } = match rkyv::from_bytes(&args) { + Ok(a) => a, + Err(_) => return SeedResponse::fail(), + }; + + let mut hash = Sha512::new(); + + hash.update(passphrase); + hash.update(b"SEED"); + + let seed = hash.finalize().to_vec(); + let seed_ptr = seed.as_ptr() as u64; + let seed_len = seed.len() as u64; + + mem::forget(seed); + + SeedResponse::success(seed_ptr, seed_len) +} + /// Computes the total balance of the given notes. /// /// The arguments are expected to be rkyv serialized [BalanceArgs] with a @@ -390,6 +422,43 @@ pub fn nullifiers(args: i32, len: i32) -> i32 { NullifiersResponse::success(nullifiers_ptr, nullifiers_len) } +impl SeedResponse { + /// Rkyv serialized length of the response + pub const LEN: usize = mem::size_of::(); + + fn as_i32_ptr(&self) -> i32 { + let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { + Ok(b) => b.into_vec(), + Err(_) => return 0, + }; + + let ptr = b.as_ptr() as i32; + mem::forget(b); + + ptr + } + + /// Returns a representation of a successful seed response. + pub fn success(seed_ptr: u64, seed_len: u64) -> i32 { + Self { + success: true, + seed_ptr, + seed_len, + } + .as_i32_ptr() + } + + /// Returns a representation of the failure of the seed operation. + pub fn fail() -> i32 { + Self { + success: false, + seed_ptr: 0, + seed_len: 0, + } + .as_i32_ptr() + } +} + impl BalanceResponse { /// Rkyv serialized length of the response pub const LEN: usize = mem::size_of::(); diff --git a/src/lib.rs b/src/lib.rs index c759197..a03949f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,26 @@ pub const MAX_KEY: usize = 24; /// The maximum allocated buffer for rkyv serialization. pub const MAX_LEN: usize = rusk_abi::ARGBUF_LEN; +/// The arguments of the seed function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct SeedArgs { + /// An arbitrary sequence of bytes used to generate a secure seed. + pub passphrase: Vec, +} + +/// The response of the seed function. +#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct SeedResponse { + /// Status of the execution + pub success: bool, + /// Computed seed pointer. + pub seed_ptr: u64, + /// The length of the computed seed. + pub seed_len: u64, +} + /// The arguments of the balance function. #[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] #[archive_attr(derive(CheckBytes))] diff --git a/tests/wallet.rs b/tests/wallet.rs index e98a86e..96e2b0e 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -9,12 +9,44 @@ use dusk_wallet_core::{ tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, FilterNotesArgs, FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, - NullifiersArgs, NullifiersResponse, ViewKeysArgs, ViewKeysResponse, - MAX_LEN, + NullifiersArgs, NullifiersResponse, SeedArgs, SeedResponse, ViewKeysArgs, + ViewKeysResponse, MAX_LEN, }; use std::collections::HashMap; use wasmer::{imports, Function, Instance, Memory, Module, Store, Value}; +#[test] +fn seed_works() { + let passphrase = + b"Taking a new step, uttering a new word, is what people fear most." + .to_vec(); + + let args = SeedArgs { passphrase }; + let args = + rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); + + let mut wallet = Wallet::default(); + + let len = Value::I32(args.len() as i32); + let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + + wallet.memory_write(ptr, &args); + + let ptr = Value::I32(ptr as i32); + let ptr = wallet.call("seed", &[ptr, len])[0].unwrap_i32() as u64; + + let response = wallet.memory_read(ptr, SeedResponse::LEN); + let response = rkyv::from_bytes::(&response) + .expect("failed to deserialize seed"); + + assert!(response.success); + + let seed = + wallet.memory_read(response.seed_ptr, response.seed_len as usize); + + assert_eq!(seed.len(), utils::RNG_SEED); +} + #[test] fn balance_works() { let seed = [0xfa; utils::RNG_SEED]; @@ -535,6 +567,7 @@ impl Default for Wallet { add_function(&mut f, &instance, "malloc"); add_function(&mut f, &instance, "free_mem"); + add_function(&mut f, &instance, "seed"); add_function(&mut f, &instance, "balance"); add_function(&mut f, &instance, "execute"); add_function(&mut f, &instance, "merge_notes"); From 7b117e6592aec5cdb2b65cf314f0a12e8e5b4845 Mon Sep 17 00:00:00 2001 From: Artifex Date: Fri, 25 Aug 2023 05:09:11 +0200 Subject: [PATCH 09/29] refactor wasm module input into json schema --- Cargo.toml | 12 +- assets/schema.json | 380 +++++++++++++++++++++++ build.rs | 43 +++ src/ffi.rs | 729 ++++++++++++++++----------------------------- src/key.rs | 18 +- src/lib.rs | 195 +----------- src/tx.rs | 93 ++++-- src/types.rs | 138 +++++++++ src/utils.rs | 88 +++++- tests/wallet.rs | 620 +++++++++++++++----------------------- 10 files changed, 1228 insertions(+), 1088 deletions(-) create mode 100644 assets/schema.json create mode 100644 build.rs create mode 100644 src/types.rs diff --git a/Cargo.toml b/Cargo.toml index 13c25a6..986247b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,18 +10,19 @@ crate-type = ["cdylib", "rlib"] [dependencies] bytecheck = { version = "0.6", default-features = false } +bs58 = { version = "0.5", default-features = false, features = ["alloc", "cb58"] } dusk-bls12_381-sign = { version = "0.4", default-features = false } +dusk-bytes = "^0.1" dusk-jubjub = { version = "0.12", default-features = false } -dusk-merkle = { version = "0.5", features = ["rkyv-impl"] } dusk-pki = { version = "0.12", default-features = false, features = ["rkyv-impl"] } -dusk-poseidon = { version = "0.30", default-features = false } dusk-schnorr = { version = "0.13", default-features = false } phoenix-core = { version = "0.20.0-rc.0", default-features = false, features = ["alloc", "rkyv-impl"] } poseidon-merkle = { version = "0.2.1-rc.0", features = ["rkyv-impl"] } -rand_core = "^0.6" rand_chacha = { version = "^0.3", default-features = false } +rand_core = "^0.6" rkyv = { version = "^0.7", default-features = false } -rusk-abi = { version = "0.10.0-piecrust.0.6", default-features = false } +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } sha2 = { version = "^0.10", default-features = false } [target.'cfg(target_family = "wasm")'.dependencies] @@ -33,3 +34,6 @@ rusk-abi = { version = "0.10.0-piecrust.0.6", default-features = false } [dev-dependencies] rand = "^0.8" wasmer = "=3.1" + +[build-dependencies] +schemafy_lib = "0.6" diff --git a/assets/schema.json b/assets/schema.json new file mode 100644 index 0000000..c4ae9ac --- /dev/null +++ b/assets/schema.json @@ -0,0 +1,380 @@ +{ + "$id": "https://json.schemastore.org/base.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "SeedArgs": { + "description": "The arguments of the seed function", + "type": "object", + "required": [ + "passphrase" + ], + "properties": { + "passphrase": { + "description": "An arbitrary sequence of bytes used to generate a secure seed", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "BalanceArgs": { + "description": "The arguments of the balance function", + "type": "object", + "required": [ + "notes", + "seed" + ], + "properties": { + "notes": { + "description": "A rkyv serialized [Vec]; all notes should have their keys derived from `seed`", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 32, + "minItems": 32 + } + } + }, + "BalanceResponse": { + "description": "The response of the balance function", + "type": "object", + "required": [ + "maximum", + "value" + ], + "properties": { + "maximum": { + "description": "Maximum value per transaction", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "value": { + "description": "Total computed balance", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "ExecuteCall": { + "description": "A call to a contract method", + "type": "object", + "required": [ + "contract", + "method", + "payload" + ], + "properties": { + "contract": { + "description": "The id of the contract to call in Base58 format", + "type": "string" + }, + "method": { + "description": "The name of the method to be called", + "type": "string" + }, + "payload": { + "description": "The payload of the call", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "OutputType": { + "description": "A note type variant", + "type": "string", + "enum": [ + "Transparent", + "Obfuscated" + ] + }, + "ExecuteOutput": { + "description": "The output of a transfer", + "type": "object", + "required": [ + "note_type", + "value", + "receiver" + ], + "properties": { + "note_type": { + "description": "The type of the note", + "$ref": "#/definitions/OutputType" + }, + "value": { + "description": "The value of the output", + "type": "integer", + "format": "uint64", + "minimum": 1 + }, + "receiver": { + "description": "The address of the receiver in Base58 format", + "type": "string" + }, + "ref_id": { + "description": "A reference id to be appended to the output", + "type": "integer", + "format": "uint64", + "minimum": 1 + } + } + }, + "PublicSpendKeyArgs": { + "description": "The arguments of the public_spend_key function", + "type": "object", + "required": [ + "idx", + "seed" + ], + "properties": { + "idx": { + "description": "The index of the public spend key", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 32, + "minItems": 32 + } + } + }, + "ExecuteArgs": { + "description": "The arguments of the execute function", + "type": "object", + "required": [ + "gas_limit", + "gas_price", + "inputs", + "openings", + "refund", + "rng_seed", + "seed" + ], + "properties": { + "call": { + "description": "A call to a contract method", + "$ref": "#/definitions/ExecuteCall" + }, + "crossover": { + "description": "The [phoenix_core::Crossover] value", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "gas_limit": { + "description": "The gas limit of the transaction", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "gas_price": { + "description": "The gas price per unit for the transaction", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "inputs": { + "description": "A rkyv serialized [Vec] to be used as inputs", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "openings": { + "description": "A rkyv serialized [Vec] to open the inputs to a Merkle root", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "output": { + "description": "The transfer output note", + "$ref": "#/definitions/ExecuteOutput" + }, + "refund": { + "description": "The refund addressin Base58 format", + "type": "string" + }, + "rng_seed": { + "description": "Seed used to derive the entropy for the notes", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 32, + "minItems": 32 + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 32, + "minItems": 32 + } + } + }, + "ExecuteResponse": { + "description": "The response of the execute function", + "type": "object", + "required": [ + "tx", + "unspent" + ], + "properties": { + "tx": { + "description": "A rkyv serialized [crate::tx::UnspentTransaction]", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "unspent": { + "description": "A rkyv serialized [Vec] containing the notes that weren't used", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "MergeNotesArgs": { + "description": "The arguments of the merge_notes function", + "type": "object", + "required": [ + "notes" + ], + "properties": { + "notes": { + "description": "All serialized list of notes to be merged", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + } + }, + "FilterNotesArgs": { + "description": "The arguments of the filter_notes function", + "type": "object", + "required": [ + "flags", + "notes" + ], + "properties": { + "flags": { + "description": "Boolean flags to be negative filtered", + "type": "array", + "items": { + "type": "boolean" + } + }, + "notes": { + "description": "Rkyv serialized notes to be filtered", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "ViewKeysArgs": { + "description": "The arguments of the view_keys function", + "type": "object", + "required": [ + "seed" + ], + "properties": { + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 32, + "minItems": 32 + } + } + }, + "NullifiersArgs": { + "description": "The arguments of the nullifiers function", + "type": "object", + "required": [ + "notes", + "seed" + ], + "properties": { + "notes": { + "description": "Rkyv serialized notes to have nullifiers generated", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 32, + "minItems": 32 + } + } + } + } +} diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ef6bcc9 --- /dev/null +++ b/build.rs @@ -0,0 +1,43 @@ +use std::fs; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=assets/schema.json"); + + let schema = PathBuf::from("assets/schema.json").canonicalize().unwrap(); + let types = PathBuf::from("src/types.rs"); + + schemafy_lib::Generator::builder() + .with_input_file(&schema) + .build() + .generate_to_file(&types) + .unwrap(); + + let types = types.canonicalize().unwrap(); + let contents = fs::read_to_string(&types).unwrap(); + + // some limitations of schemafy will not allow it to parse the correct + // integer type as they incorrectly fallback any integer to `i64` + let contents = contents.replace("Vec", "Vec"); + let contents = contents.replace("i64", "u64"); + + let header = r#"// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Arguments and responses to the module requests + +// THIS FILE IS AUTO GENERATED!! + +#![allow(missing_docs)] + +use alloc::vec::Vec; +use alloc::string::String; +use serde::{Serialize, Deserialize};"#; + + let contents = header.to_owned() + &contents; + + fs::write(&types, contents).unwrap(); +} diff --git a/src/ffi.rs b/src/ffi.rs index f715895..04b424b 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -9,20 +9,12 @@ use alloc::{vec, vec::Vec}; use core::mem; -use dusk_pki::PublicSpendKey; -use phoenix_core::note::{ArchivedNote, Note, NoteType}; +use dusk_bytes::Serializable; +use phoenix_core::note::{ArchivedNote, Note}; use rkyv::validation::validators::FromBytesError; use sha2::{Digest, Sha512}; -use crate::{ - key, tx, utils, ArchivedBalanceResponse, ArchivedExecuteResponse, - ArchivedFilterNotesResponse, ArchivedMergeNotesResponse, - ArchivedNullifiersResponse, ArchivedSeedResponse, ArchivedViewKeysResponse, - BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, - FilterNotesArgs, FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, - NullifiersArgs, NullifiersResponse, SeedArgs, SeedResponse, ViewKeysArgs, - ViewKeysResponse, MAX_KEY, MAX_LEN, -}; +use crate::{key, tx, types, utils, MAX_KEY, MAX_LEN}; /// Allocates a buffer of `len` bytes on the WASM memory. #[no_mangle] @@ -45,18 +37,15 @@ pub fn free_mem(ptr: i32, len: i32) { /// Computes a secure seed from the given passphrase. /// -/// The arguments are expected to be rkyv serialized [SeedArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::SeedArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the seed. #[no_mangle] -pub fn seed(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; - - let SeedArgs { passphrase } = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return SeedResponse::fail(), +pub fn seed(args: i32, len: i32) -> i64 { + let types::SeedArgs { passphrase } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), }; let mut hash = Sha512::new(); @@ -65,33 +54,65 @@ pub fn seed(args: i32, len: i32) -> i32 { hash.update(b"SEED"); let seed = hash.finalize().to_vec(); - let seed_ptr = seed.as_ptr() as u64; - let seed_len = seed.len() as u64; + let ptr = seed.as_ptr() as u32; + let len = seed.len() as u32; mem::forget(seed); + utils::compose(true, ptr, len) +} + +/// Computes the public spend key from the given seed/index pair. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::PublicSpendKeyArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the Base58 +/// representation of the public spend key. +#[no_mangle] +pub fn public_spend_key(args: i32, len: i32) -> i64 { + let types::PublicSpendKeyArgs { idx, seed } = + match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; + + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; - SeedResponse::success(seed_ptr, seed_len) + let psk = key::derive_psk(&seed, idx); + let psk = bs58::encode(psk.to_bytes()).into_string(); + + let ptr = psk.as_ptr() as u32; + let len = psk.len() as u32; + + mem::forget(psk); + utils::compose(true, ptr, len) } /// Computes the total balance of the given notes. /// -/// The arguments are expected to be rkyv serialized [BalanceArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::BalanceArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to JSON string +/// representing [types::BalanceResult]. #[no_mangle] -pub fn balance(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; +pub fn balance(args: i32, len: i32) -> i64 { + let types::BalanceArgs { notes, seed } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; - let BalanceArgs { seed, notes } = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return BalanceResponse::fail(), + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), }; let notes: Vec = match rkyv::from_bytes(¬es) { Ok(n) => utils::sanitize_notes(n), - Err(_) => return BalanceResponse::fail(), + Err(_) => return utils::fail(), }; let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; @@ -115,7 +136,7 @@ pub fn balance(args: i32, len: i32) -> i32 { } } - return BalanceResponse::fail(); + return utils::fail(); } // the top 4 notes are the maximum value a transaction can have, given the @@ -123,147 +144,162 @@ pub fn balance(args: i32, len: i32) -> i32 { values.sort_by(|a, b| b.cmp(a)); let maximum = values.iter().take(4).sum::(); - BalanceResponse::success(sum, maximum) + utils::into_ptr(types::BalanceResponse { + maximum, + value: sum, + }) } /// Computes a serialized unproven transaction from the given arguments. /// -/// The arguments are expected to be rkyv serialized [ExecuteArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::ExecuteArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to JSON string +/// representing [types::ExecuteResult]. #[no_mangle] -pub fn execute(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; - let args = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return ExecuteResponse::fail(), +pub fn execute(args: i32, len: i32) -> i64 { + let types::ExecuteArgs { + call, + crossover, + gas_limit, + gas_price, + inputs, + openings, + output, + refund, + rng_seed, + seed, + } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; + + let inputs: Vec = match rkyv::from_bytes(&inputs) { + Ok(n) => utils::sanitize_notes(n), + Err(_) => return utils::fail(), }; - fn inner( - ExecuteArgs { - seed, - rng_seed, - inputs, - openings, - refund, - output, - crossover, - gas_limit, - gas_price, - call, - }: ExecuteArgs, - ) -> Option<(Vec, Vec)> { - let inputs: Vec = rkyv::from_bytes(&inputs).ok()?; - let inputs = utils::sanitize_notes(inputs); - let openings: Vec = rkyv::from_bytes(&openings).ok()?; - let refund: PublicSpendKey = rkyv::from_bytes(&refund).ok()?; - let output: Option = rkyv::from_bytes(&output).ok()?; - let call: Option = rkyv::from_bytes(&call).ok()?; - - let value = output.as_ref().map(|o| o.value).unwrap_or(0); - let total_output = - gas_limit.saturating_mul(gas_price).saturating_add(value); - - let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; - let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; - let mut keys_len = 0; - let mut openings = openings.into_iter(); - let mut full_inputs = Vec::with_capacity(inputs.len()); - - 'outer: for input in inputs { - // we iterate all the available keys until one can successfully - // decrypt the note. if any fails, returns false - for idx in 0..=MAX_KEY { - if keys_len == idx { - keys_ssk[idx] = key::derive_ssk(&seed, idx as u64); - keys[idx] = keys_ssk[idx].view_key(); - keys_len += 1; - } - - if let Ok(value) = input.value(Some(&keys[idx])) { - let opening = openings.next()?; - full_inputs.push((input, opening, value, idx)); - continue 'outer; - } + let openings: Vec = match rkyv::from_bytes(&openings) { + Ok(n) => n, + Err(_) => return utils::fail(), + }; + + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; + + let rng_seed = match utils::sanitize_seed(rng_seed) { + Some(s) => s, + None => return utils::fail(), + }; + + let value = output.as_ref().map(|o| o.value).unwrap_or(0); + let total_output = + gas_limit.saturating_mul(gas_price).saturating_add(value); + + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_len = 0; + let mut openings = openings.into_iter(); + let mut full_inputs = Vec::with_capacity(inputs.len()); + + 'outer: for input in inputs { + // we iterate all the available keys until one can successfully + // decrypt the note. if any fails, returns false + for idx in 0..=MAX_KEY { + if keys_len == idx { + keys_ssk[idx] = key::derive_ssk(&seed, idx as u64); + keys[idx] = keys_ssk[idx].view_key(); + keys_len += 1; } - return None; - } + if let Ok(value) = input.value(Some(&keys[idx])) { + let opening = match openings.next() { + Some(o) => o, + None => return utils::fail(), + }; - // optimizes the inputs given the total amount - let (unspent, inputs) = utils::knapsack(full_inputs, total_output)?; - let inputs: Vec<_> = inputs - .into_iter() - .map(|(note, opening, value, idx)| tx::PreInput { - note, - opening, - value, - ssk: &keys_ssk[idx], - }) - .collect(); - let total_input: u64 = inputs.iter().map(|i| i.value).sum(); - let total_refund = total_input.saturating_sub(total_output); - - let mut outputs: Vec = Vec::with_capacity(2); - if let Some(o) = output { - outputs.push(o); - } - if total_refund > 0 { - outputs.push(tx::OutputValue { - r#type: NoteType::Obfuscated, - value: total_refund, - receiver: refund, - ref_id: 0, - }); + full_inputs.push((input, opening, value, idx)); + continue 'outer; + } } - let rng = &mut utils::rng(&rng_seed); - let tx = tx::UnprovenTransaction::new( - rng, inputs, outputs, &refund, gas_limit, gas_price, crossover, - call, - )?; + return utils::fail(); + } - let unspent = rkyv::to_bytes::<_, MAX_LEN>(&unspent).ok()?.into_vec(); - let tx = rkyv::to_bytes::<_, MAX_LEN>(&tx).ok()?.into_vec(); + // optimizes the inputs given the total amount + let (unspent, inputs) = match utils::knapsack(full_inputs, total_output) { + Some(k) => k, + None => return utils::fail(), + }; - Some((unspent, tx)) + let inputs: Vec<_> = inputs + .into_iter() + .map(|(note, opening, value, idx)| tx::PreInput { + note, + opening, + value, + ssk: &keys_ssk[idx], + }) + .collect(); + + let total_input: u64 = inputs.iter().map(|i| i.value).sum(); + let total_refund = total_input.saturating_sub(total_output); + + let mut outputs = Vec::with_capacity(2); + if let Some(o) = output { + outputs.push(o); + } + if total_refund > 0 { + outputs.push(types::ExecuteOutput { + note_type: types::OutputType::Obfuscated, + receiver: refund.clone(), + ref_id: None, + value: total_refund, + }); } - let (unspent, tx) = match inner(args) { + let rng = &mut utils::rng(&rng_seed); + let tx = tx::UnprovenTransaction::new( + rng, inputs, outputs, refund, gas_limit, gas_price, crossover, call, + ); + let tx = match tx { Some(t) => t, - None => return ExecuteResponse::fail(), + None => return utils::fail(), }; - let unspent_ptr = unspent.as_ptr() as u64; - let unspent_len = unspent.len() as u64; - let tx_ptr = tx.as_ptr() as u64; - let tx_len = tx.len() as u64; + let unspent = match rkyv::to_bytes::<_, MAX_LEN>(&unspent).ok() { + Some(t) => t.into_vec(), + None => return utils::fail(), + }; - mem::forget(unspent); - mem::forget(tx); + let tx = match rkyv::to_bytes::<_, MAX_LEN>(&tx).ok() { + Some(t) => t.into_vec(), + None => return utils::fail(), + }; - ExecuteResponse::success(unspent_ptr, unspent_len, tx_ptr, tx_len) + utils::into_ptr(types::ExecuteResponse { tx, unspent }) } /// Merges many lists of serialized notes into a unique, sanitized set. /// -/// The arguments are expected to be rkyv serialized [MergeNotesArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::MergeNotesArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. #[no_mangle] -pub fn merge_notes(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; - - let MergeNotesArgs { notes } = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return MergeNotesResponse::fail(), +pub fn merge_notes(args: i32, len: i32) -> i64 { + let types::MergeNotesArgs { notes } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), }; - let len = 3 * notes.len() / mem::size_of::() / 2; + let len = + 3usize.saturating_mul(notes.len()) / mem::size_of::() / 2; + let notes = match notes .into_iter() .map(|n| rkyv::from_bytes::>(&n)) @@ -275,45 +311,40 @@ pub fn merge_notes(args: i32, len: i32) -> i32 { }, ) { Ok(n) => n, - Err(_) => return MergeNotesResponse::fail(), + Err(_) => return utils::fail(), }; let notes = match rkyv::to_bytes::<_, MAX_LEN>(¬es) { Ok(n) => n.into_vec(), - Err(_) => return MergeNotesResponse::fail(), + Err(_) => return utils::fail(), }; - let notes_ptr = notes.as_ptr() as u64; - let notes_len = notes.len() as u64; + let ptr = notes.as_ptr() as u32; + let len = notes.len() as u32; - MergeNotesResponse::success(notes_ptr, notes_len) + mem::forget(notes); + utils::compose(true, ptr, len) } /// Filters a list of notes from a list of negative flags. The flags that are /// `true` will represent a note that must be removed from the set. /// -/// The arguments are expected to be rkyv serialized [FilterNotesArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::FilterNotesArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. #[no_mangle] -pub fn filter_notes(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; - - let FilterNotesArgs { notes, flags } = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return FilterNotesResponse::fail(), - }; +pub fn filter_notes(args: i32, len: i32) -> i64 { + let types::FilterNotesArgs { flags, notes } = + match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; let notes: Vec = match rkyv::from_bytes(¬es) { Ok(n) => n, - Err(_) => return FilterNotesResponse::fail(), - }; - - let flags: Vec = match rkyv::from_bytes(&flags) { - Ok(f) => f, - Err(_) => return FilterNotesResponse::fail(), + Err(_) => return utils::fail(), }; let notes: Vec<_> = notes @@ -325,29 +356,33 @@ pub fn filter_notes(args: i32, len: i32) -> i32 { let notes = utils::sanitize_notes(notes); let notes = match rkyv::to_bytes::<_, MAX_LEN>(¬es) { Ok(n) => n.into_vec(), - Err(_) => return FilterNotesResponse::fail(), + Err(_) => return utils::fail(), }; - let notes_ptr = notes.as_ptr() as u64; - let notes_len = notes.len() as u64; + let ptr = notes.as_ptr() as u32; + let len = notes.len() as u32; - FilterNotesResponse::success(notes_ptr, notes_len) + mem::forget(notes); + utils::compose(true, ptr, len) } /// Returns a list of [ViewKey] that belongs to this wallet. /// -/// The arguments are expected to be rkyv serialized [ViewKeysArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::ViewKeysArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. #[no_mangle] -pub fn view_keys(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; +pub fn view_keys(args: i32, len: i32) -> i64 { + let types::ViewKeysArgs { seed } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; - let ViewKeysArgs { seed } = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return ViewKeysResponse::fail(), + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), }; let vks: Vec<_> = (0..=MAX_KEY) @@ -356,35 +391,40 @@ pub fn view_keys(args: i32, len: i32) -> i32 { let vks = match rkyv::to_bytes::<_, MAX_LEN>(&vks) { Ok(k) => k.into_vec(), - Err(_) => return ViewKeysResponse::fail(), + Err(_) => return utils::fail(), }; - let vks_ptr = vks.as_ptr() as u64; - let vks_len = vks.len() as u64; + let ptr = vks.as_ptr() as u32; + let len = vks.len() as u32; - ViewKeysResponse::success(vks_ptr, vks_len) + mem::forget(vks); + utils::compose(true, ptr, len) } /// Returns a list of [BlsScalar] nullifiers for the given [Vec] combined /// with the keys of this wallet. /// -/// The arguments are expected to be rkyv serialized [NullifiersArgs] with a -/// pointer defined via [malloc]. It will consume the `args` allocated region -/// and drop it. +/// Expects as argument a fat pointer to a JSON string representing +/// [types::NullifiersArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. #[no_mangle] -pub fn nullifiers(args: i32, len: i32) -> i32 { - let args = args as *mut u8; - let len = len as usize; - let args = unsafe { Vec::from_raw_parts(args, len, len) }; - - let NullifiersArgs { seed, notes } = match rkyv::from_bytes(&args) { - Ok(a) => a, - Err(_) => return NullifiersResponse::fail(), - }; +pub fn nullifiers(args: i32, len: i32) -> i64 { + let types::NullifiersArgs { notes, seed } = + match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; let notes: Vec = match rkyv::from_bytes(¬es) { Ok(n) => n, - Err(_) => return NullifiersResponse::fail(), + Err(_) => return utils::fail(), + }; + + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), }; let mut nullifiers = Vec::with_capacity(notes.len()); @@ -408,286 +448,17 @@ pub fn nullifiers(args: i32, len: i32) -> i32 { } } - return NullifiersResponse::fail(); + return utils::fail(); } let nullifiers = match rkyv::to_bytes::<_, MAX_LEN>(&nullifiers) { Ok(n) => n.into_vec(), - Err(_) => return NullifiersResponse::fail(), + Err(_) => return utils::fail(), }; - let nullifiers_ptr = nullifiers.as_ptr() as u64; - let nullifiers_len = nullifiers.len() as u64; - - NullifiersResponse::success(nullifiers_ptr, nullifiers_len) -} - -impl SeedResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); + let ptr = nullifiers.as_ptr() as u32; + let len = nullifiers.len() as u32; - ptr - } - - /// Returns a representation of a successful seed response. - pub fn success(seed_ptr: u64, seed_len: u64) -> i32 { - Self { - success: true, - seed_ptr, - seed_len, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the seed operation. - pub fn fail() -> i32 { - Self { - success: false, - seed_ptr: 0, - seed_len: 0, - } - .as_i32_ptr() - } -} - -impl BalanceResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); - - ptr - } - - /// Returns a representation of a successful balance operation with the - /// computed value. - pub fn success(value: u64, maximum: u64) -> i32 { - Self { - success: true, - value, - maximum, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the balance operation. - pub fn fail() -> i32 { - Self { - success: false, - value: 0, - maximum: 0, - } - .as_i32_ptr() - } -} - -impl ExecuteResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); - - ptr - } - - /// Returns a representation of a successful execute operation with the - /// underlying unspent notes list and the unproven transaction. - pub fn success( - unspent_ptr: u64, - unspent_len: u64, - tx_ptr: u64, - tx_len: u64, - ) -> i32 { - Self { - success: true, - unspent_ptr, - unspent_len, - tx_ptr, - tx_len, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the execute operation. - pub fn fail() -> i32 { - Self { - success: false, - unspent_ptr: 0, - unspent_len: 0, - tx_ptr: 0, - tx_len: 0, - } - .as_i32_ptr() - } -} - -impl MergeNotesResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); - - ptr - } - - /// Returns a representation of a successful merge_notes operation. - pub fn success(notes_ptr: u64, notes_len: u64) -> i32 { - Self { - success: true, - notes_ptr, - notes_len, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the merge_notes operation. - pub fn fail() -> i32 { - Self { - success: false, - notes_ptr: 0, - notes_len: 0, - } - .as_i32_ptr() - } -} - -impl FilterNotesResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); - - ptr - } - - /// Returns a representation of a successful filter_notes operation. - pub fn success(notes_ptr: u64, notes_len: u64) -> i32 { - Self { - success: true, - notes_ptr, - notes_len, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the filter_notes operation. - pub fn fail() -> i32 { - Self { - success: false, - notes_ptr: 0, - notes_len: 0, - } - .as_i32_ptr() - } -} - -impl ViewKeysResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); - - ptr - } - - /// Returns a representation of a successful view_keys operation. - pub fn success(vks_ptr: u64, vks_len: u64) -> i32 { - Self { - success: true, - vks_ptr, - vks_len, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the view_keys operation. - pub fn fail() -> i32 { - Self { - success: false, - vks_ptr: 0, - vks_len: 0, - } - .as_i32_ptr() - } -} - -impl NullifiersResponse { - /// Rkyv serialized length of the response - pub const LEN: usize = mem::size_of::(); - - fn as_i32_ptr(&self) -> i32 { - let b = match rkyv::to_bytes::<_, MAX_LEN>(self) { - Ok(b) => b.into_vec(), - Err(_) => return 0, - }; - - let ptr = b.as_ptr() as i32; - mem::forget(b); - - ptr - } - - /// Returns a representation of a successful nullifiers operation. - pub fn success(nullifiers_ptr: u64, nullifiers_len: u64) -> i32 { - Self { - success: true, - nullifiers_ptr, - nullifiers_len, - } - .as_i32_ptr() - } - - /// Returns a representation of the failure of the nullifiers operation. - pub fn fail() -> i32 { - Self { - success: false, - nullifiers_ptr: 0, - nullifiers_len: 0, - } - .as_i32_ptr() - } + mem::forget(nullifiers); + utils::compose(true, ptr, len) } diff --git a/src/key.rs b/src/key.rs index e4af0f8..d6fb81f 100644 --- a/src/key.rs +++ b/src/key.rs @@ -6,10 +6,10 @@ //! Utilities to derive keys from the seed. -use crate::utils; +use crate::{utils, RNG_SEED}; use dusk_bls12_381_sign::SecretKey; -use dusk_pki::{SecretSpendKey, ViewKey}; +use dusk_pki::{PublicSpendKey, SecretSpendKey, ViewKey}; /// Generates a secret spend key from its seed and index. /// @@ -17,7 +17,7 @@ use dusk_pki::{SecretSpendKey, ViewKey}; /// `index` are passed through SHA-256. A constant is then mixed in and the /// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is /// subsequently used to generate the key. -pub fn derive_ssk(seed: &[u8; utils::RNG_SEED], index: u64) -> SecretSpendKey { +pub fn derive_ssk(seed: &[u8; RNG_SEED], index: u64) -> SecretSpendKey { SecretSpendKey::random(&mut utils::rng_with_index(seed, index)) } @@ -27,14 +27,22 @@ pub fn derive_ssk(seed: &[u8; utils::RNG_SEED], index: u64) -> SecretSpendKey { /// `index` are passed through SHA-256. A constant is then mixed in and the /// resulting hash is then used to seed a `ChaCha12` CSPRNG, which is /// subsequently used to generate the key. -pub fn derive_sk(seed: &[u8; utils::RNG_SEED], index: u64) -> SecretKey { +pub fn derive_sk(seed: &[u8; RNG_SEED], index: u64) -> SecretKey { SecretKey::random(&mut utils::rng_with_index(seed, index)) } +/// Generates a public spend key from its seed and index. +/// +/// The secret spend key is derived from [derive_ssk], and then the key is +/// generated via [SecretSpendKey::public_spend_key]. +pub fn derive_psk(seed: &[u8; RNG_SEED], index: u64) -> PublicSpendKey { + derive_ssk(seed, index).public_spend_key() +} + /// Generates a view key from its seed and index. /// /// The secret spend key is derived from [derive_ssk], and then the key is /// generated via [SecretSpendKey::view_key]. -pub fn derive_vk(seed: &[u8; utils::RNG_SEED], index: u64) -> ViewKey { +pub fn derive_vk(seed: &[u8; RNG_SEED], index: u64) -> ViewKey { derive_ssk(seed, index).view_key() } diff --git a/src/lib.rs b/src/lib.rs index a03949f..df998c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,14 +10,10 @@ extern crate alloc; -use alloc::vec::Vec; - -use bytecheck::CheckBytes; -use rkyv::{Archive, Deserialize, Serialize}; - pub mod ffi; pub mod key; pub mod tx; +pub mod types; pub mod utils; /// The maximum number of keys (inclusive) to derive when attempting to decrypt @@ -27,189 +23,8 @@ pub const MAX_KEY: usize = 24; /// The maximum allocated buffer for rkyv serialization. pub const MAX_LEN: usize = rusk_abi::ARGBUF_LEN; -/// The arguments of the seed function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct SeedArgs { - /// An arbitrary sequence of bytes used to generate a secure seed. - pub passphrase: Vec, -} - -/// The response of the seed function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct SeedResponse { - /// Status of the execution - pub success: bool, - /// Computed seed pointer. - pub seed_ptr: u64, - /// The length of the computed seed. - pub seed_len: u64, -} - -/// The arguments of the balance function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct BalanceArgs { - /// Seed used to derive the keys of the wallet. - pub seed: [u8; utils::RNG_SEED], - /// A rkyv serialized [Vec]; all notes should - /// have their keys derived from `seed`. - pub notes: Vec, -} - -/// The response of the balance function. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, -)] -#[archive_attr(derive(CheckBytes))] -pub struct BalanceResponse { - /// Status of the execution - pub success: bool, - /// Total computed balance - pub value: u64, - /// Maximum value per transaction - pub maximum: u64, -} - -/// The arguments of the execute function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct ExecuteArgs { - /// Seed used to derive the keys of the wallet. - pub seed: [u8; utils::RNG_SEED], - /// Seed used to derive the entropy for the notes. - pub rng_seed: [u8; utils::RNG_SEED], - /// A rkyv serialized [Vec] to be used as inputs - pub inputs: Vec, - /// A rkyv serialized [Vec] to open the inputs to a Merkle - /// root. - pub openings: Vec, - /// A rkyv serialize [dusk_pki::PublicSpendKey] to whom the remainder - /// balance will be refunded - pub refund: Vec, - /// A rkyv serialized [Option] to define the receiver. - pub output: Vec, - /// The [phoenix_core::Crossover] value; will be skipped if `0`. - pub crossover: u64, - /// The gas limit of the transaction. - pub gas_limit: u64, - /// The gas price per unit for the transaction. - pub gas_price: u64, - /// A rkyv serialized [Option] to perform contract calls. - pub call: Vec, -} - -/// The response of the execute function. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, -)] -#[archive_attr(derive(CheckBytes))] -pub struct ExecuteResponse { - /// Status of the execution - pub success: bool, - /// The pointer to a rkyv serialized [Vec>] - /// containing the notes that weren't used. - pub unspent_ptr: u64, - /// The length of the rkyv serialized `unspent_ptr`. - pub unspent_len: u64, - /// The pointer to a rkyv serialized [tx::UnspentTransaction]. - pub tx_ptr: u64, - /// The length of the rkyv serialized `tx_ptr`. - pub tx_len: u64, -} - -/// The arguments of the merge_notes function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct MergeNotesArgs { - /// All serialized list of notes to be merged. - pub notes: Vec>, -} - -/// The response of the merge_notes function. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, -)] -#[archive_attr(derive(CheckBytes))] -pub struct MergeNotesResponse { - /// Status of the execution - pub success: bool, - /// The pointer to a rkyv serialized [Vec>] - /// containing the merged notes set. - pub notes_ptr: u64, - /// The length of the rkyv serialized `notes_ptr`. - pub notes_len: u64, -} - -/// The arguments of the filter_notes function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct FilterNotesArgs { - /// Rkyv serialized notes to be filtered. - pub notes: Vec, - /// Rkyv serialized boolean flags to be negative filtered. - pub flags: Vec, -} - -/// The response of the filter_notes function. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, -)] -#[archive_attr(derive(CheckBytes))] -pub struct FilterNotesResponse { - /// Status of the execution - pub success: bool, - /// The pointer to a rkyv serialized [Vec>] - /// containing the filtered notes set. - pub notes_ptr: u64, - /// The length of the rkyv serialized `notes_ptr`. - pub notes_len: u64, -} - -/// The arguments of the view_keys function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct ViewKeysArgs { - /// Seed used to derive the keys of the wallet. - pub seed: [u8; utils::RNG_SEED], -} - -/// The response of the view_keys function. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, -)] -#[archive_attr(derive(CheckBytes))] -pub struct ViewKeysResponse { - /// Status of the execution - pub success: bool, - /// The pointer to a rkyv serialized [Vec>]. - pub vks_ptr: u64, - /// The length of the rkyv serialized `vks_ptr`. - pub vks_len: u64, -} - -/// The arguments of the nullifiers function. -#[derive(Debug, Clone, PartialEq, Eq, Archive, Serialize, Deserialize)] -#[archive_attr(derive(CheckBytes))] -pub struct NullifiersArgs { - /// Seed used to derive the keys of the wallet. - pub seed: [u8; utils::RNG_SEED], - /// Rkyv serialized notes to have nullifiers generated. - pub notes: Vec, -} +/// Length of the seed of the generated rng. +pub const RNG_SEED: usize = 64; -/// The response of the view_keys function. -#[derive( - Debug, Clone, Copy, PartialEq, Eq, Archive, Serialize, Deserialize, -)] -#[archive_attr(derive(CheckBytes))] -pub struct NullifiersResponse { - /// Status of the execution - pub success: bool, - /// The pointer to a rkyv serialized [Vec>] containing the - /// nullifiers ordered list. - pub nullifiers_ptr: u64, - /// The length of the rkyv serialized `nullifiers_ptr`. - pub nullifiers_len: u64, -} +/// The length of the allocated response. +pub const RESPONSE_LEN: usize = 3 * i32::BITS as usize / 8; diff --git a/src/tx.rs b/src/tx.rs index ecf9452..384f7cc 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -8,6 +8,7 @@ use alloc::string::String; use alloc::vec::Vec; +use core::mem; use bytecheck::CheckBytes; use dusk_jubjub::{ @@ -23,6 +24,8 @@ use rkyv::{Archive, Deserialize, Serialize}; use rusk_abi::hash::Hasher; use rusk_abi::{ContractId, POSEIDON_TREE_DEPTH}; +use crate::{types, utils}; + /// Chosen arity for the Notes tree implementation. pub const POSEIDON_TREE_ARITY: usize = 4; @@ -139,16 +142,16 @@ impl UnprovenTransaction { rng: &mut Rng, inputs: I, outputs: O, - refund: &'a PublicSpendKey, + refund: String, gas_limit: u64, gas_price: u64, - crossover: u64, - call: Option, + crossover: Option, + call: Option, ) -> Option where Rng: RngCore + CryptoRng, I: IntoIterator>, - O: IntoIterator, + O: IntoIterator, { let (nullifiers, inputs): (Vec<_>, Vec<_>) = inputs .into_iter() @@ -159,39 +162,71 @@ impl UnprovenTransaction { .unzip(); let anchor = inputs.first().map(|i| i.opening.root().hash)?; - let (output_notes, outputs): (Vec<_>, Vec<_>) = outputs - .into_iter() - .map(|o| { - let r = JubJubScalar::random(rng); - let blinder = JubJubScalar::random(rng); - let nonce = BlsScalar::from(o.ref_id); - let note = Note::deterministic( - o.r#type, - &r, - nonce, - &o.receiver, - o.value, - blinder, - ); - Output { - note, - value: o.value, - blinder, - } - }) - .map(|o| (o.note.clone(), o)) - .unzip(); + let refund = utils::bs58_to_psk(&refund)?; + + let mut output_notes = Vec::with_capacity(4); + let mut outputs_values = Vec::with_capacity(4); + + for types::ExecuteOutput { + note_type, + receiver, + ref_id, + value, + } in outputs.into_iter() + { + let r#type = match note_type { + types::OutputType::Transparent => NoteType::Transparent, + types::OutputType::Obfuscated => NoteType::Obfuscated, + }; + let r = JubJubScalar::random(rng); + let blinder = JubJubScalar::random(rng); + let nonce = BlsScalar::from(ref_id.unwrap_or_default()); + let receiver = utils::bs58_to_psk(&receiver)?; + let note = Note::deterministic( + r#type, &r, nonce, &receiver, value, blinder, + ); + + output_notes.push(note.clone()); + outputs_values.push(Output { + note, + value, + blinder, + }); + } + + let outputs = outputs_values; + + let call = match call { + Some(types::ExecuteCall { + contract, + method, + payload, + }) => { + let decoded = bs58::decode(contract).into_vec().ok()?; + if decoded.len() != mem::size_of::() { + return None; + } + let mut contract = ContractId::uninitialized(); + contract.as_bytes_mut().copy_from_slice(&decoded); + Some(CallData { + contract, + method, + payload, + }) + } + None => None, + }; let call_phoenix = call.as_ref().map(|c| { (c.contract.to_bytes(), c.method.clone(), c.payload.clone()) }); - let fee = Fee::new(rng, gas_limit, gas_price, refund); + let fee = Fee::new(rng, gas_limit, gas_price, &refund); - let crossover = (crossover > 0).then(|| { + let crossover = crossover.map(|crossover| { let blinder = JubJubScalar::random(rng); let (_, crossover_note) = - Note::obfuscated(rng, refund, crossover, blinder) + Note::obfuscated(rng, &refund, crossover, blinder) .try_into() .expect("Obfuscated notes should always yield crossovers"); Crossover { diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..18cea7f --- /dev/null +++ b/src/types.rs @@ -0,0 +1,138 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Arguments and responses to the module requests + +// THIS FILE IS AUTO GENERATED!! + +#![allow(missing_docs)] + +use alloc::string::String; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +#[doc = " The arguments of the balance function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct BalanceArgs { + #[doc = " A rkyv serialized [Vec]; all notes should have their keys derived from "] + #[doc = " `seed`"] + pub notes: Vec, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " The response of the balance function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct BalanceResponse { + #[doc = " Maximum value per transaction"] + pub maximum: u64, + #[doc = " Total computed balance"] + pub value: u64, +} +#[doc = " The arguments of the execute function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteArgs { + #[doc = " A call to a contract method"] + #[serde(skip_serializing_if = "Option::is_none")] + pub call: Option, + #[doc = " The [phoenix_core::Crossover] value"] + #[serde(skip_serializing_if = "Option::is_none")] + pub crossover: Option, + #[doc = " The gas limit of the transaction"] + pub gas_limit: u64, + #[doc = " The gas price per unit for the transaction"] + pub gas_price: u64, + #[doc = " A rkyv serialized [Vec] to be used as inputs"] + pub inputs: Vec, + #[doc = " A rkyv serialized [Vec] to open the inputs to a Merkle root"] + pub openings: Vec, + #[doc = " The transfer output note"] + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, + #[doc = " The refund addressin Base58 format"] + pub refund: String, + #[doc = " Seed used to derive the entropy for the notes"] + pub rng_seed: Vec, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " A call to a contract method"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteCall { + #[doc = " The id of the contract to call in Base58 format"] + pub contract: String, + #[doc = " The name of the method to be called"] + pub method: String, + #[doc = " The payload of the call"] + pub payload: Vec, +} +#[doc = " The output of a transfer"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteOutput { + #[doc = " The type of the note"] + pub note_type: OutputType, + #[doc = " The address of the receiver in Base58 format"] + pub receiver: String, + #[doc = " A reference id to be appended to the output"] + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_id: Option, + #[doc = " The value of the output"] + pub value: u64, +} +#[doc = " The response of the execute function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteResponse { + #[doc = " A rkyv serialized [crate::tx::UnspentTransaction]"] + pub tx: Vec, + #[doc = " A rkyv serialized [Vec] containing the notes that weren't used"] + pub unspent: Vec, +} +#[doc = " The arguments of the filter_notes function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct FilterNotesArgs { + #[doc = " Boolean flags to be negative filtered"] + pub flags: Vec, + #[doc = " Rkyv serialized notes to be filtered"] + pub notes: Vec, +} +#[doc = " The arguments of the merge_notes function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct MergeNotesArgs { + #[doc = " All serialized list of notes to be merged"] + pub notes: Vec>, +} +#[doc = " The arguments of the nullifiers function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct NullifiersArgs { + #[doc = " Rkyv serialized notes to have nullifiers generated"] + pub notes: Vec, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " A note type variant"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub enum OutputType { + Transparent, + Obfuscated, +} +#[doc = " The arguments of the public_spend_key function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct PublicSpendKeyArgs { + #[doc = " The index of the public spend key"] + pub idx: u64, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " The arguments of the seed function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct SeedArgs { + #[doc = " An arbitrary sequence of bytes used to generate a secure seed"] + pub passphrase: Vec, +} +#[doc = " The arguments of the view_keys function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ViewKeysArgs { + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} diff --git a/src/utils.rs b/src/utils.rs index 6383cf8..8d9f9c3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,17 +6,83 @@ //! Misc utilities required by the library implementation. -use crate::tx; +use crate::{tx, RNG_SEED}; use alloc::vec::Vec; +use core::mem; +use dusk_bytes::DeserializableSlice; +use dusk_pki::PublicSpendKey; use phoenix_core::Note; use rand_chacha::ChaCha12Rng; use rand_core::SeedableRng; +use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -/// Length of the seed of the generated rng. -pub const RNG_SEED: usize = 64; +/// Composes a `i64` from the provided arguments. This will be returned from the +/// WASM module functions. +pub const fn compose(success: bool, ptr: u32, len: u32) -> i64 { + let success = (!success) as u64; + let ptr = (ptr as u64) << 32; + let len = ((len as u64) << 48) >> 32; + (success | ptr | len) as i64 +} + +/// Decomposes a `i64` into its inner arguments, being: +/// +/// - status: a boolean indicating the success of the operation +/// - ptr: a pointer to the underlying data +/// - len: the length of the underlying data +pub const fn decompose(result: i64) -> (bool, u64, u64) { + let ptr = (result >> 32) as u64; + let len = ((result << 32) >> 48) as u64; + let success = ((result << 63) >> 63) == 0; + + (success, ptr, len) +} + +/// Takes a JSON string from the memory slice and deserializes it into the +/// provided type. +pub fn take_args(args: i32, len: i32) -> Option +where + T: for<'a> Deserialize<'a>, +{ + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + let args = alloc::string::String::from_utf8(args).ok()?; + serde_json::from_str(&args).ok() +} + +/// Sanitizes arbitrary bytes into well-formed seed. +pub fn sanitize_seed(bytes: Vec) -> Option<[u8; RNG_SEED]> { + (bytes.len() == RNG_SEED).then(|| { + let mut seed = [0u8; RNG_SEED]; + seed.copy_from_slice(&bytes); + seed + }) +} + +/// Fails the operation +pub const fn fail() -> i64 { + compose(false, 0, 0) +} + +/// Converts the provided response into an allocated pointer and returns the +/// composed success value. +pub fn into_ptr(response: T) -> i64 +where + T: Serialize, +{ + let response = serde_json::to_string(&response).unwrap_or_default(); + let ptr = response.as_ptr() as u32; + let len = response.len() as u32; + let result = compose(true, ptr, len); + + mem::forget(response); + + result +} /// Creates a secure RNG from a seed. pub fn rng(seed: &[u8; RNG_SEED]) -> ChaCha12Rng { @@ -48,6 +114,13 @@ pub fn sanitize_notes(mut notes: Vec) -> Vec { notes } +/// Converts a Base58 string into a [PublicSpendKey]. +pub fn bs58_to_psk(psk: &str) -> Option { + // TODO this should be defined in dusk-pki + let bytes = bs58::decode(psk).into_vec().ok()?; + PublicSpendKey::from_reader(&mut &bytes[..]).ok() +} + /// Perform a knapsack algorithm to define the notes to be used as input. /// /// Returns a tuple containing (unspent, inputs). `unspent` contains the notes @@ -83,6 +156,15 @@ pub fn knapsack( Some((unspent, nodes)) } +#[test] +fn compose_works() { + assert_eq!(decompose(compose(true, 0, 0)), (true, 0, 0)); + assert_eq!(decompose(compose(false, 0, 0)), (false, 0, 0)); + assert_eq!(decompose(compose(false, 1, 0)), (false, 1, 0)); + assert_eq!(decompose(compose(false, 0, 1)), (false, 0, 1)); + assert_eq!(decompose(compose(false, 4837, 383)), (false, 4837, 383)); +} + #[test] fn knapsack_works() { use core::mem; diff --git a/tests/wallet.rs b/tests/wallet.rs index 96e2b0e..ca95217 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -6,148 +6,123 @@ //! Wallet library tests. -use dusk_wallet_core::{ - tx, utils, BalanceArgs, BalanceResponse, ExecuteArgs, ExecuteResponse, - FilterNotesArgs, FilterNotesResponse, MergeNotesArgs, MergeNotesResponse, - NullifiersArgs, NullifiersResponse, SeedArgs, SeedResponse, ViewKeysArgs, - ViewKeysResponse, MAX_LEN, -}; -use std::collections::HashMap; -use wasmer::{imports, Function, Instance, Memory, Module, Store, Value}; +use dusk_bytes::Serializable; +use dusk_pki::PublicSpendKey; +use dusk_wallet_core::{tx, types, utils, MAX_LEN, RNG_SEED}; +use rusk_abi::ContractId; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use wasmer::{imports, Instance, Module, Store, Value}; #[test] fn seed_works() { - let passphrase = - b"Taking a new step, uttering a new word, is what people fear most." - .to_vec(); - - let args = SeedArgs { passphrase }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); - let mut wallet = Wallet::default(); - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - - wallet.memory_write(ptr, &args); - - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("seed", &[ptr, len])[0].unwrap_i32() as u64; - - let response = wallet.memory_read(ptr, SeedResponse::LEN); - let response = rkyv::from_bytes::(&response) - .expect("failed to deserialize seed"); + let seed = wallet.call("seed", json!({ + "passphrase": b"Taking a new step, uttering a new word, is what people fear most.".to_vec() + })).take_memory(); - assert!(response.success); - - let seed = - wallet.memory_read(response.seed_ptr, response.seed_len as usize); - - assert_eq!(seed.len(), utils::RNG_SEED); + assert_eq!(seed.len(), RNG_SEED); } #[test] fn balance_works() { - let seed = [0xfa; utils::RNG_SEED]; + let seed = [0xfa; RNG_SEED]; let values = [10, 250, 15, 39, 55]; - - let notes = node::notes(&seed, values); - - let args = BalanceArgs { seed, notes }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); - let mut wallet = Wallet::default(); - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - - wallet.memory_write(ptr, &args); + let types::BalanceResponse { maximum, value } = wallet + .call( + "balance", + json!({ + "notes": node::notes(&seed, values), + "seed": seed.to_vec(), + }), + ) + .take_contents(); + + assert_eq!(value, values.into_iter().sum::()); + assert_eq!(maximum, 359); +} - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("balance", &[ptr, len])[0].unwrap_i32() as u64; - let balance = wallet.memory_read(ptr, BalanceResponse::LEN); - let balance = rkyv::from_bytes::(&balance) - .expect("failed to deserialize balance"); +#[test] +fn public_spend_key_works() { + let seed = [0xfa; RNG_SEED]; - let ptr = Value::I32(ptr as i32); - let len = Value::I32(BalanceResponse::LEN as i32); - wallet.call("free_mem", &[ptr, len]); + let mut wallet = Wallet::default(); - assert!(balance.success); - assert_eq!(balance.value, values.into_iter().sum::()); - assert_eq!(balance.maximum, 359); + let psk = wallet + .call( + "public_spend_key", + json!({ + "seed": seed.to_vec(), + "idx": 3 + }), + ) + .take_memory(); + + let psk = bs58::decode(psk).into_vec().unwrap(); + let mut psk_array = [0u8; PublicSpendKey::SIZE]; + + psk_array.copy_from_slice(&psk); + PublicSpendKey::from_bytes(&psk_array).unwrap(); } #[test] fn execute_works() { - let seed = [0xfa; utils::RNG_SEED]; - let rng_seed = [0xfb; utils::RNG_SEED]; + let seed = [0xfa; RNG_SEED]; + let rng_seed = [0xfb; RNG_SEED]; let values = [10, 250, 15, 7500]; - let (inputs, openings) = node::notes_and_openings(&seed, values); - let refund = node::psk(&seed); - let output = node::output(&seed, 133); - let crossover = 35; - let gas_limit = 100; - let gas_price = 2; - let call = node::empty_call_data(); - let args = ExecuteArgs { - seed, - rng_seed, - inputs, - openings, - refund, - output, - crossover, - gas_limit, - gas_price, - call, - }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); - let mut wallet = Wallet::default(); - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - - wallet.memory_write(ptr, &args); - - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("execute", &[ptr, len])[0].unwrap_i32() as u64; - let execute = wallet.memory_read(ptr, ExecuteResponse::LEN); - let execute = rkyv::from_bytes::(&execute) - .expect("failed to deserialize execute"); - - let ptr = Value::I32(ptr as i32); - let len = Value::I32(ExecuteResponse::LEN as i32); - wallet.call("free_mem", &[ptr, len]); - - let unspent = - wallet.memory_read(execute.unspent_ptr, execute.unspent_len as usize); - let _unspent: Vec = - rkyv::from_bytes(&unspent).expect("failed to deserialize notes"); + let psk = wallet + .call( + "public_spend_key", + json!({ + "seed": seed.to_vec(), + "idx":5 + }), + ) + .take_memory(); + let psk = String::from_utf8(psk).unwrap(); + + let mut contract = ContractId::uninitialized(); + contract.as_bytes_mut().iter_mut().for_each(|b| *b = 0xfa); + let contract = bs58::encode(contract.as_bytes()).into_string(); - let ptr = Value::I32(execute.unspent_ptr as i32); - let len = Value::I32(execute.unspent_len as i32); - wallet.call("free_mem", &[ptr, len]); - - let tx = wallet.memory_read(execute.tx_ptr, execute.tx_len as usize); - let _tx: tx::UnprovenTransaction = - rkyv::from_bytes(&tx).expect("failed to deserialize tx"); - - let ptr = Value::I32(execute.tx_ptr as i32); - let len = Value::I32(execute.tx_len as i32); - wallet.call("free_mem", &[ptr, len]); - - assert!(execute.success); + let (inputs, openings) = node::notes_and_openings(&seed, values); + let args = json!({ + "call": { + "contract": contract, + "method": "commit", + "payload": b"We lost because we told ourselves we lost.".to_vec(), + }, + "crossover": 25, + "gas_limit": 100, + "gas_price": 2, + "inputs": inputs, + "openings": openings, + "output": { + "note_type": "Transparent", + "receiver": psk.clone(), + "ref_id": 15, + "value": 10, + }, + "refund": psk, + "rng_seed": rng_seed.to_vec(), + "seed": seed.to_vec() + }); + let types::ExecuteResponse { tx, unspent } = + wallet.call("execute", args).take_contents(); + + rkyv::from_bytes::(&tx).unwrap(); + rkyv::from_bytes::>(&unspent).unwrap(); } #[test] fn merge_notes_works() { - let seed = [0xfa; utils::RNG_SEED]; + let seed = [0xfa; RNG_SEED]; let notes1 = node::raw_notes(&seed, [10, 250, 15, 39, 55]); let notes2 = vec![notes1[1].clone(), notes1[3].clone()]; @@ -174,39 +149,20 @@ fn merge_notes_works() { let notes3 = rkyv::to_bytes::<_, MAX_LEN>(¬es3).unwrap().into_vec(); let notes = vec![notes1, notes2, notes3]; - let args = MergeNotesArgs { notes }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); - let mut wallet = Wallet::default(); - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + let notes = wallet + .call("merge_notes", json!({ "notes": notes })) + .take_memory(); - wallet.memory_write(ptr, &args); + let notes = rkyv::from_bytes::>(¬es).unwrap(); - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("merge_notes", &[ptr, len])[0].unwrap_i32() as u64; - - let notes = wallet.memory_read(ptr, MergeNotesResponse::LEN); - let notes = rkyv::from_bytes::(¬es) - .expect("failed to deserialize merged notes"); - - let ptr = Value::I32(ptr as i32); - let len = Value::I32(MergeNotesResponse::LEN as i32); - wallet.call("free_mem", &[ptr, len]); - - let merged = wallet.memory_read(notes.notes_ptr, notes.notes_len as usize); - let merged: Vec = - rkyv::from_bytes(&merged).expect("failed to deserialize notes"); - - assert!(notes.success); - assert_eq!(merged, notes_merged); + assert_eq!(notes, notes_merged); } #[test] fn filter_notes_works() { - let seed = [0xfa; utils::RNG_SEED]; + let seed = [0xfa; RNG_SEED]; let notes = node::raw_notes(&seed, [10, 250, 15, 39, 55]); let flags = vec![true, true, false, true, false]; @@ -214,103 +170,39 @@ fn filter_notes_works() { let filtered = utils::sanitize_notes(filtered); let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es).unwrap().into_vec(); - let flags = rkyv::to_bytes::<_, MAX_LEN>(&flags).unwrap().into_vec(); - - let args = FilterNotesArgs { notes, flags }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); let mut wallet = Wallet::default(); - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; + let notes = wallet + .call("filter_notes", json!({ "flags": flags, "notes": notes })) + .take_memory(); - wallet.memory_write(ptr, &args); - - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("filter_notes", &[ptr, len])[0].unwrap_i32() as u64; - - let response = wallet.memory_read(ptr, FilterNotesResponse::LEN); - let response = rkyv::from_bytes::(&response) - .expect("failed to deserialize filtered notes"); - - assert!(response.success); - - let notes = - wallet.memory_read(response.notes_ptr, response.notes_len as usize); - let notes: Vec = - rkyv::from_bytes(¬es).expect("failed to deserialize notes"); - - let ptr = Value::I32(ptr as i32); - let len = Value::I32(FilterNotesResponse::LEN as i32); - wallet.call("free_mem", &[ptr, len]); + let notes = rkyv::from_bytes::>(¬es).unwrap(); assert_eq!(notes, filtered); } #[test] fn view_keys_works() { - let seed = [0xfa; utils::RNG_SEED]; - - let args = ViewKeysArgs { seed }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); - - let keys = { - let mut wallet = Wallet::default(); - - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - - wallet.memory_write(ptr, &args); - - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("view_keys", &[ptr, len])[0].unwrap_i32() as u64; + let seed = [0xfa; RNG_SEED]; - let response = wallet.memory_read(ptr, ViewKeysResponse::LEN); - let response = rkyv::from_bytes::(&response) - .expect("failed to deserialize view keys"); - - assert!(response.success); - - let keys = - wallet.memory_read(response.vks_ptr, response.vks_len as usize); - let keys: Vec = - rkyv::from_bytes(&keys).expect("failed to deserialize keys"); - keys - }; - - let keys_p = { - let mut wallet = Wallet::default(); - - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - - wallet.memory_write(ptr, &args); - - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("view_keys", &[ptr, len])[0].unwrap_i32() as u64; - - let response = wallet.memory_read(ptr, ViewKeysResponse::LEN); - let response = rkyv::from_bytes::(&response) - .expect("failed to deserialize view keys"); - - assert!(response.success); + let mut wallet = Wallet::default(); - let keys = - wallet.memory_read(response.vks_ptr, response.vks_len as usize); - let keys: Vec = - rkyv::from_bytes(&keys).expect("failed to deserialize keys"); - keys - }; + let vk = wallet + .call( + "view_keys", + json!({ + "seed": seed.to_vec() + }), + ) + .take_memory(); - // assert keys generation is deterministic - assert_eq!(keys, keys_p); + rkyv::from_bytes::>(&vk).unwrap(); } #[test] fn nullifiers_works() { - let seed = [0xfa; utils::RNG_SEED]; + let seed = [0xfa; RNG_SEED]; let (notes, nullifiers): (Vec<_>, Vec<_>) = node::raw_notes_and_nulifiers(&seed, [10, 250, 15, 39, 55]) @@ -318,30 +210,21 @@ fn nullifiers_works() { .unzip(); let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es).unwrap().into_vec(); - let args = NullifiersArgs { seed, notes }; - let args = - rkyv::to_bytes::<_, MAX_LEN>(&args).expect("failed to serialize args"); let mut wallet = Wallet::default(); - let len = Value::I32(args.len() as i32); - let ptr = wallet.call("malloc", &[len.clone()])[0].unwrap_i32() as u64; - - wallet.memory_write(ptr, &args); - - let ptr = Value::I32(ptr as i32); - let ptr = wallet.call("nullifiers", &[ptr, len])[0].unwrap_i32() as u64; - - let response = wallet.memory_read(ptr, NullifiersResponse::LEN); - let response = rkyv::from_bytes::(&response) - .expect("failed to deserialize nullifiers"); - - assert!(response.success); - let response = wallet - .memory_read(response.nullifiers_ptr, response.nullifiers_len as usize); - let response: Vec = - rkyv::from_bytes(&response).expect("failed to deserialize nullifiers"); + .call( + "nullifiers", + json!({ + "seed": seed.to_vec(), + "notes": notes + }), + ) + .take_memory(); + + let response = + rkyv::from_bytes::>(&response).unwrap(); assert_eq!(nullifiers, response); } @@ -351,14 +234,11 @@ mod node { use core::mem; use dusk_jubjub::{BlsScalar, JubJubScalar}; - use dusk_wallet_core::{key, tx, utils, MAX_KEY, MAX_LEN}; - use phoenix_core::{Note, NoteType}; + use dusk_wallet_core::{key, tx, utils, MAX_KEY, MAX_LEN, RNG_SEED}; + use phoenix_core::Note; use rand::RngCore; - pub fn raw_notes( - seed: &[u8; utils::RNG_SEED], - values: Values, - ) -> Vec + pub fn raw_notes(seed: &[u8; RNG_SEED], values: Values) -> Vec where Values: IntoIterator, { @@ -380,8 +260,38 @@ mod node { .collect() } + pub fn notes(seed: &[u8; RNG_SEED], values: Values) -> Vec + where + Values: IntoIterator, + { + rkyv::to_bytes::<_, MAX_LEN>(&raw_notes(seed, values)) + .expect("failed to serialize notes") + .into_vec() + } + + pub fn notes_and_openings( + seed: &[u8; RNG_SEED], + values: Values, + ) -> (Vec, Vec) + where + Values: IntoIterator, + { + let values: Vec<_> = values.into_iter().collect(); + let len = values.len(); + let notes = notes(seed, values); + let openings: Vec<_> = (0..len) + .map(|_| unsafe { mem::zeroed::() }) + .collect(); + + let openings = rkyv::to_bytes::<_, MAX_LEN>(&openings) + .expect("failed to serialize openings") + .into_vec(); + + (notes, openings) + } + pub fn raw_notes_and_nulifiers( - seed: &[u8; utils::RNG_SEED], + seed: &[u8; RNG_SEED], values: Values, ) -> Vec<(Note, BlsScalar)> where @@ -408,123 +318,111 @@ mod node { }) .collect() } +} - pub fn notes( - seed: &[u8; utils::RNG_SEED], - values: Values, - ) -> Vec - where - Values: IntoIterator, - { - rkyv::to_bytes::<_, MAX_LEN>(&raw_notes(seed, values)) - .expect("failed to serialize notes") - .into_vec() - } +pub struct Wallet { + pub store: Store, + pub module: Module, + pub instance: Instance, +} - pub fn notes_and_openings( - seed: &[u8; utils::RNG_SEED], - values: Values, - ) -> (Vec, Vec) - where - Values: IntoIterator, - { - let rng = &mut utils::rng(seed); - let notes: Vec<_> = values - .into_iter() - .map(|value| { - let obfuscated = (rng.next_u32() & 1) == 1; - let idx = rng.next_u64() % MAX_KEY as u64; - let psk = key::derive_ssk(seed, idx).public_spend_key(); +pub struct CallResult<'a> { + pub status: bool, + pub val: u64, + pub aux: u64, + pub wallet: &'a mut Wallet, +} - if obfuscated { - let blinder = JubJubScalar::random(rng); - Note::obfuscated(rng, &psk, value, blinder) - } else { - Note::transparent(rng, &psk, value) - } - }) - .collect(); +impl<'a> CallResult<'a> { + pub fn new(wallet: &'a mut Wallet, value: i64) -> Self { + let (status, val, aux) = utils::decompose(value); + Self { + status, + val, + aux, + wallet, + } + } - let openings: Vec<_> = (0..notes.len()) - .map(|_| unsafe { mem::zeroed::() }) - .collect(); + pub fn take_memory(self) -> Vec { + assert!(self.status); - let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es) - .expect("failed to serialize notes") - .into_vec(); + let mut bytes = vec![0u8; self.aux as usize]; - let openings = rkyv::to_bytes::<_, MAX_LEN>(&openings) - .expect("failed to serialize openings") - .into_vec(); + self.wallet + .instance + .exports + .get_memory("memory") + .unwrap() + .view(&self.wallet.store) + .read(self.val, &mut bytes) + .unwrap(); - (notes, openings) - } + self.wallet + .instance + .exports + .get_function("free_mem") + .unwrap() + .call( + &mut self.wallet.store, + &[Value::I32(self.val as i32), Value::I32(self.aux as i32)], + ) + .unwrap(); - pub fn psk(seed: &[u8; utils::RNG_SEED]) -> Vec { - let psk = key::derive_ssk(seed, 0).public_spend_key(); - rkyv::to_bytes::<_, MAX_LEN>(&psk) - .expect("failed to serialize psk") - .into_vec() + bytes } - pub fn output(seed: &[u8; utils::RNG_SEED], value: u64) -> Vec { - let rng = &mut utils::rng(seed); - let obfuscated = (rng.next_u32() & 1) == 1; - let r#type = if obfuscated { - NoteType::Obfuscated - } else { - NoteType::Transparent - }; - let receiver = key::derive_ssk(seed, 1).public_spend_key(); - let ref_id = rng.next_u64(); - let output = Some(tx::OutputValue { - r#type, - value, - receiver, - ref_id, - }); - - rkyv::to_bytes::<_, MAX_LEN>(&output) - .expect("failed to serialize notes") - .into_vec() + pub fn take_contents(self) -> T + where + T: for<'b> Deserialize<'b>, + { + assert!(self.status); + let bytes = self.take_memory(); + let json = String::from_utf8(bytes).unwrap(); + serde_json::from_str(&json).unwrap() } - pub fn empty_call_data() -> Vec { - let call: Option = None; - rkyv::to_bytes::<_, MAX_LEN>(&call) - .expect("failed to serialize call data") - .into_vec() + pub fn take_val(self) -> u64 { + assert!(self.status); + self.val } } -pub struct Wallet { - pub store: Store, - pub module: Module, - pub memory: Memory, - pub f: HashMap<&'static str, Function>, -} - impl Wallet { - pub fn call(&mut self, f: &str, args: &[Value]) -> Box<[Value]> { - self.f[f] - .call(&mut self.store, args) - .expect("failed to call module function") - } + pub fn call(&mut self, f: &str, args: T) -> CallResult + where + T: Serialize, + { + let bytes = serde_json::to_string(&args).unwrap(); + let len = Value::I32(bytes.len() as i32); + let ptr = self + .instance + .exports + .get_function("malloc") + .unwrap() + .call(&mut self.store, &[len.clone()]) + .unwrap()[0] + .unwrap_i32(); - pub fn memory_write(&mut self, ptr: u64, data: &[u8]) { - self.memory + self.instance + .exports + .get_memory("memory") + .unwrap() .view(&self.store) - .write(ptr, data) - .expect("failed to write memory"); - } + .write(ptr as u64, bytes.as_bytes()) + .unwrap(); - pub fn memory_read(&self, ptr: u64, len: usize) -> Vec { - let mut bytes = vec![0u8; len]; - self.memory - .view(&self.store) - .read(ptr, &mut bytes) - .expect("failed to read memory"); - bytes + let ptr = Value::I32(ptr); + let result = self + .instance + .exports + .get_function(f) + .unwrap() + .call(&mut self.store, &[ptr, len]) + .unwrap()[0] + .unwrap_i64(); + + CallResult::new(self, result) } } @@ -542,44 +440,10 @@ impl Default for Wallet { let instance = Instance::new(&mut store, &module, &import_object) .expect("failed to instanciate the wasm module"); - let memory = instance - .exports - .get_memory("memory") - .expect("failed to get instance memory") - .clone(); - - fn add_function( - map: &mut HashMap<&'static str, Function>, - instance: &Instance, - name: &'static str, - ) { - map.insert( - name, - instance - .exports - .get_function(name) - .expect("failed to import wasm function") - .clone(), - ); - } - - let mut f = HashMap::new(); - - add_function(&mut f, &instance, "malloc"); - add_function(&mut f, &instance, "free_mem"); - add_function(&mut f, &instance, "seed"); - add_function(&mut f, &instance, "balance"); - add_function(&mut f, &instance, "execute"); - add_function(&mut f, &instance, "merge_notes"); - add_function(&mut f, &instance, "filter_notes"); - add_function(&mut f, &instance, "view_keys"); - add_function(&mut f, &instance, "nullifiers"); - Self { store, module, - memory, - f, + instance, } } } From 8753b50a4d01b6966a175e4c666dc32fe1efb2c8 Mon Sep 17 00:00:00 2001 From: Artifex Date: Fri, 25 Aug 2023 05:10:35 +0200 Subject: [PATCH 10/29] missing header --- build.rs | 6 ++++++ src/types.rs | 5 ++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/build.rs b/build.rs index ef6bcc9..25950a0 100644 --- a/build.rs +++ b/build.rs @@ -1,3 +1,9 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + use std::fs; use std::path::PathBuf; diff --git a/src/types.rs b/src/types.rs index 18cea7f..9931c44 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,10 +10,9 @@ #![allow(missing_docs)] -use alloc::string::String; use alloc::vec::Vec; -use serde::{Deserialize, Serialize}; -#[doc = " The arguments of the balance function"] +use alloc::string::String; +use serde::{Serialize, Deserialize};#[doc = " The arguments of the balance function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct BalanceArgs { #[doc = " A rkyv serialized [Vec]; all notes should have their keys derived from "] From 8a647daad801ba221cdd0a73eeefad605d381e84 Mon Sep 17 00:00:00 2001 From: Artifex Date: Fri, 25 Aug 2023 16:57:34 +0200 Subject: [PATCH 11/29] tweak makefile to generate asset package --- Makefile | 28 +++++++++++++++++----------- README.md | 4 ++++ src/types.rs | 5 +++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/Makefile b/Makefile index 9f3eb19..ba3d2d1 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,10 @@ +PROJECT := $(shell sed -n '0,/name\s*=\s*"\(.*\)"/s/name\s*=\s*"\(.*\)"/\1/p' Cargo.toml) +VERSION := $(shell sed -n '0,/version\s*=\s*"\(.*\)"/s/version\s*=\s*"\(.*\)"/\1/p' Cargo.toml) +FLAGS := RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" +WASM := "target/wasm32-unknown-unknown/release/$(shell sed -n '0,/name\s*=\s*"\(.*\)"/s/name\s*=\s*"\(.*\)"/\1/p' Cargo.toml | sed 's/-/_/g').wasm" +NPM_WASM := "mod.wasm" +PACKAGE := "assets/$(PROJECT)-$(VERSION).wasm" + help: ## Display this help screen @grep -h \ -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ @@ -7,16 +14,15 @@ test: wasm ## Run the wasmer tests @cargo test wasm: ## Build the WASM files - @RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" \ - cargo build \ - --release \ - --color=always \ - -Z build-std=core,alloc,panic_abort \ - -Z build-std-features=panic_immediate_abort \ - --target wasm32-unknown-unknown + @$(FLAGS) cargo build --release \ + --target wasm32-unknown-unknown \ + --color=always \ + -Z build-std=core,alloc,panic_abort \ + -Z build-std-features=panic_immediate_abort -package: ## Prepare the WASM npm package - wasm-opt -O4 target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ - -o mod.wasm +package: wasm ## Prepare the WASM package + @wasm-opt -O3 $(WASM) -o $(NPM_WASM) + @cp $(NPM_WASM) $(PACKAGE) + @echo "Package created: $(PACKAGE)" -.PHONY: test wasm help +.PHONY: test wasm package help diff --git a/README.md b/README.md index 0d5cb8b..e3cf2b0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ A library for generating and dealing with transactions. +## Requirements + +To generate packages, we use [binaryen](https://github.com/WebAssembly/binaryen). + ## Build To build and test the crate: diff --git a/src/types.rs b/src/types.rs index 9931c44..18cea7f 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,9 +10,10 @@ #![allow(missing_docs)] -use alloc::vec::Vec; use alloc::string::String; -use serde::{Serialize, Deserialize};#[doc = " The arguments of the balance function"] +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; +#[doc = " The arguments of the balance function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct BalanceArgs { #[doc = " A rkyv serialized [Vec]; all notes should have their keys derived from "] From ce2703b0377710ea32f0217d4babed3f4a66069f Mon Sep 17 00:00:00 2001 From: Artifex Date: Sat, 26 Aug 2023 22:30:26 +0200 Subject: [PATCH 12/29] integrate with wallet-cli tests --- Cargo.toml | 4 +-- README.md | 41 ++++++++++++++++++------ assets/schema.json | 28 ++++++++-------- rust-toolchain | 2 +- src/ffi.rs | 80 ++++++++++++---------------------------------- src/tx.rs | 2 +- src/types.rs | 4 +-- src/utils.rs | 19 ++++++++++- tests/wallet.rs | 3 +- 9 files changed, 92 insertions(+), 91 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 986247b..aad3afe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dusk-wallet-core" -version = "0.20.0-piecrust.0.6" +version = "0.21.0" edition = "2021" description = "The core functionality of the Dusk wallet" license = "MPL-2.0" @@ -20,7 +20,7 @@ phoenix-core = { version = "0.20.0-rc.0", default-features = false, features = [ poseidon-merkle = { version = "0.2.1-rc.0", features = ["rkyv-impl"] } rand_chacha = { version = "^0.3", default-features = false } rand_core = "^0.6" -rkyv = { version = "^0.7", default-features = false } +rkyv = { version = "^0.7", default-features = false, features = ["size_32"] } serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } serde_json = { version = "1.0", default-features = false, features = ["alloc"] } sha2 = { version = "^0.10", default-features = false } diff --git a/README.md b/README.md index e3cf2b0..f3aa62c 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,46 @@ [![codecov](https://codecov.io/gh/dusk-network/wallet-core/branch/main/graph/badge.svg?token=9W3J09AWZG)](https://codecov.io/gh/dusk-network/wallet-core) [![documentation](https://img.shields.io/badge/docs-wallet-blue?logo=rust)](https://docs.rs/dusk-wallet-core/) -A library for generating and dealing with transactions. +A WASM library to provide business logic for Dusk wallet implementations. + +Check the available methods under the [FFI](src/ffi.rs) module. + +Every function expects a fat pointer to its arguments already allocated to the WASM memory. For the arguments definition, check the [JSON Schema](assets/schema.json). It will consume this pointer region and free it after execution. The return of the function will also be in accordance to the schema, and the user will have to free the memory himself after fetching the data. + +For maximum compatibility, every WASM function returns a `i64` with the status of the operation and an embedded pointer. The structure of the bytes in big-endian is as follows: + +[(pointer) x 4bytes (length) x 3bytes (status) x 1bit] + +The pointer will be a maximum `u32` number, and the length a `u24` number. The status of the operation is the least significant bit of the number, and will be `0` if the operation is successful. + +Here is an algorithm to split the result into meaningful parts: + +```rust +let ptr = (result >> 32) as u64; +let len = ((result << 32) >> 48) as u64; +let success = ((result << 63) >> 63) == 0; +``` + +For an example usage, check the [wallet-cli](https://github.com/dusk-network/wallet-cli) implementation that consumes this library. ## Requirements -To generate packages, we use [binaryen](https://github.com/WebAssembly/binaryen). +- [Rust 1.71.0](https://www.rust-lang.org/) +- [target.wasm32-unknown-unknown](https://github.com/rustwasm/) +- [binaryen](https://github.com/WebAssembly/binaryen) to generate packages ## Build -To build and test the crate: +To build a distributable package: -```shell -cargo b -cargo t --all-features +```sh +make package ``` -To build the WASM module: +## Test + +To run the tests, there is an automated Makefile script -```shell -cargo b --release --target wasm32-unknown-unknown +```sh +make test ``` diff --git a/assets/schema.json b/assets/schema.json index c4ae9ac..6f995db 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -45,8 +45,8 @@ "format": "uint8", "minimum": 0 }, - "maxItems": 32, - "minItems": 32 + "maxItems": 64, + "minItems": 64 } } }, @@ -161,8 +161,8 @@ "format": "uint8", "minimum": 0 }, - "maxItems": 32, - "minItems": 32 + "maxItems": 64, + "minItems": 64 } } }, @@ -235,8 +235,8 @@ "format": "uint8", "minimum": 0 }, - "maxItems": 32, - "minItems": 32 + "maxItems": 64, + "minItems": 64 }, "seed": { "description": "Seed used to derive the keys of the wallet", @@ -246,8 +246,8 @@ "format": "uint8", "minimum": 0 }, - "maxItems": 32, - "minItems": 32 + "maxItems": 64, + "minItems": 64 } } }, @@ -316,7 +316,7 @@ } }, "notes": { - "description": "Rkyv serialized notes to be filtered", + "description": "A rkyv serialized [Vec] to be filtered", "type": "array", "items": { "type": "integer", @@ -341,8 +341,8 @@ "format": "uint8", "minimum": 0 }, - "maxItems": 32, - "minItems": 32 + "maxItems": 64, + "minItems": 64 } } }, @@ -355,7 +355,7 @@ ], "properties": { "notes": { - "description": "Rkyv serialized notes to have nullifiers generated", + "description": "A rkyv serialized [Vec] to have nullifiers generated", "type": "array", "items": { "type": "integer", @@ -371,8 +371,8 @@ "format": "uint8", "minimum": 0 }, - "maxItems": 32, - "minItems": 32 + "maxItems": 64, + "minItems": 64 } } } diff --git a/rust-toolchain b/rust-toolchain index 67946b1..c309192 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2023-01-05 +nightly-2023-05-22 diff --git a/src/ffi.rs b/src/ffi.rs index 04b424b..1dcd7ac 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -10,8 +10,7 @@ use alloc::{vec, vec::Vec}; use core::mem; use dusk_bytes::Serializable; -use phoenix_core::note::{ArchivedNote, Note}; -use rkyv::validation::validators::FromBytesError; +use phoenix_core::Note; use sha2::{Digest, Sha512}; use crate::{key, tx, types, utils, MAX_KEY, MAX_LEN}; @@ -156,7 +155,7 @@ pub fn balance(args: i32, len: i32) -> i64 { /// [types::ExecuteArgs]. /// /// Will return a triplet (status, ptr, len) pointing to JSON string -/// representing [types::ExecuteResult]. +/// representing [types::ExecuteResponse]. #[no_mangle] pub fn execute(args: i32, len: i32) -> i64 { let types::ExecuteArgs { @@ -196,8 +195,10 @@ pub fn execute(args: i32, len: i32) -> i64 { }; let value = output.as_ref().map(|o| o.value).unwrap_or(0); - let total_output = - gas_limit.saturating_mul(gas_price).saturating_add(value); + let total_output = gas_limit + .saturating_mul(gas_price) + .saturating_add(value) + .saturating_add(crossover.unwrap_or_default()); let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; @@ -297,33 +298,19 @@ pub fn merge_notes(args: i32, len: i32) -> i64 { None => return utils::fail(), }; - let len = - 3usize.saturating_mul(notes.len()) / mem::size_of::() / 2; - - let notes = match notes - .into_iter() - .map(|n| rkyv::from_bytes::>(&n)) - .try_fold::<_, _, Result<_, FromBytesError>>>( - Vec::with_capacity(len), - |mut set, notes| { - set.extend(notes?); - Ok(utils::sanitize_notes(set)) - }, - ) { - Ok(n) => n, - Err(_) => return utils::fail(), - }; - - let notes = match rkyv::to_bytes::<_, MAX_LEN>(¬es) { - Ok(n) => n.into_vec(), - Err(_) => return utils::fail(), - }; + let mut list = Vec::with_capacity(10); + for notes in notes { + if !notes.is_empty() { + match rkyv::from_bytes::>(¬es) { + Ok(n) => list.extend(n), + Err(_) => return utils::fail(), + }; + } + } - let ptr = notes.as_ptr() as u32; - let len = notes.len() as u32; + let notes = utils::sanitize_notes(list); - mem::forget(notes); - utils::compose(true, ptr, len) + utils::rkyv_into_ptr(notes) } /// Filters a list of notes from a list of negative flags. The flags that are @@ -354,16 +341,7 @@ pub fn filter_notes(args: i32, len: i32) -> i64 { .collect(); let notes = utils::sanitize_notes(notes); - let notes = match rkyv::to_bytes::<_, MAX_LEN>(¬es) { - Ok(n) => n.into_vec(), - Err(_) => return utils::fail(), - }; - - let ptr = notes.as_ptr() as u32; - let len = notes.len() as u32; - - mem::forget(notes); - utils::compose(true, ptr, len) + utils::rkyv_into_ptr(notes) } /// Returns a list of [ViewKey] that belongs to this wallet. @@ -389,16 +367,7 @@ pub fn view_keys(args: i32, len: i32) -> i64 { .map(|idx| key::derive_vk(&seed, idx as u64)) .collect(); - let vks = match rkyv::to_bytes::<_, MAX_LEN>(&vks) { - Ok(k) => k.into_vec(), - Err(_) => return utils::fail(), - }; - - let ptr = vks.as_ptr() as u32; - let len = vks.len() as u32; - - mem::forget(vks); - utils::compose(true, ptr, len) + utils::rkyv_into_ptr(vks) } /// Returns a list of [BlsScalar] nullifiers for the given [Vec] combined @@ -451,14 +420,5 @@ pub fn nullifiers(args: i32, len: i32) -> i64 { return utils::fail(); } - let nullifiers = match rkyv::to_bytes::<_, MAX_LEN>(&nullifiers) { - Ok(n) => n.into_vec(), - Err(_) => return utils::fail(), - }; - - let ptr = nullifiers.as_ptr() as u32; - let len = nullifiers.len() as u32; - - mem::forget(nullifiers); - utils::compose(true, ptr, len) + utils::rkyv_into_ptr(nullifiers) } diff --git a/src/tx.rs b/src/tx.rs index 384f7cc..8c89930 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -187,7 +187,7 @@ impl UnprovenTransaction { r#type, &r, nonce, &receiver, value, blinder, ); - output_notes.push(note.clone()); + output_notes.push(note); outputs_values.push(Output { note, value, diff --git a/src/types.rs b/src/types.rs index 18cea7f..40e8a6d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -93,7 +93,7 @@ pub struct ExecuteResponse { pub struct FilterNotesArgs { #[doc = " Boolean flags to be negative filtered"] pub flags: Vec, - #[doc = " Rkyv serialized notes to be filtered"] + #[doc = " A rkyv serialized [Vec] to be filtered"] pub notes: Vec, } #[doc = " The arguments of the merge_notes function"] @@ -105,7 +105,7 @@ pub struct MergeNotesArgs { #[doc = " The arguments of the nullifiers function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct NullifiersArgs { - #[doc = " Rkyv serialized notes to have nullifiers generated"] + #[doc = " A rkyv serialized [Vec] to have nullifiers generated"] pub notes: Vec, #[doc = " Seed used to derive the keys of the wallet"] pub seed: Vec, diff --git a/src/utils.rs b/src/utils.rs index 8d9f9c3..b6b4608 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,7 +6,7 @@ //! Misc utilities required by the library implementation. -use crate::{tx, RNG_SEED}; +use crate::{tx, MAX_LEN, RNG_SEED}; use alloc::vec::Vec; use core::mem; @@ -84,6 +84,23 @@ where result } +/// Returns the provided bytes as a pointer +pub fn rkyv_into_ptr(value: T) -> i64 +where + T: rkyv::Serialize>, +{ + let bytes = match rkyv::to_bytes(&value) { + Ok(t) => t.into_vec(), + Err(_) => return fail(), + }; + + let ptr = bytes.as_ptr() as u32; + let len = bytes.len() as u32; + + mem::forget(bytes); + compose(true, ptr, len) +} + /// Creates a secure RNG from a seed. pub fn rng(seed: &[u8; RNG_SEED]) -> ChaCha12Rng { let mut hash = Sha256::new(); diff --git a/tests/wallet.rs b/tests/wallet.rs index ca95217..f47df35 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -147,7 +147,8 @@ fn merge_notes_works() { let notes1 = rkyv::to_bytes::<_, MAX_LEN>(¬es1).unwrap().into_vec(); let notes2 = rkyv::to_bytes::<_, MAX_LEN>(¬es2).unwrap().into_vec(); let notes3 = rkyv::to_bytes::<_, MAX_LEN>(¬es3).unwrap().into_vec(); - let notes = vec![notes1, notes2, notes3]; + let notes4 = vec![]; + let notes = vec![notes1, notes2, notes3, notes4]; let mut wallet = Wallet::default(); From 0ec45f1c6e24f32f59eba499e543e910bff14653 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sat, 26 Aug 2023 22:41:10 +0200 Subject: [PATCH 13/29] ignore minimal readme example in tests --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3aa62c..ebda641 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ The pointer will be a maximum `u32` number, and the length a `u24` number. The s Here is an algorithm to split the result into meaningful parts: -```rust +```rust,ignore let ptr = (result >> 32) as u64; let len = ((result << 32) >> 48) as u64; let success = ((result << 63) >> 63) == 0; From e7f8dc862eb49325656591652bd720f3bd54149c Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 02:37:06 +0200 Subject: [PATCH 14/29] remove unused public spend key wallet method --- assets/schema.json | 63 ++++++++++++++++++++++++++-------------------- build.rs | 4 ++- src/ffi.rs | 58 +++++++++++++++++++++--------------------- src/types.rs | 17 ++++++++----- tests/wallet.rs | 61 ++++++++++++++++++++++---------------------- 5 files changed, 109 insertions(+), 94 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 6f995db..7c418f1 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -139,33 +139,6 @@ } } }, - "PublicSpendKeyArgs": { - "description": "The arguments of the public_spend_key function", - "type": "object", - "required": [ - "idx", - "seed" - ], - "properties": { - "idx": { - "description": "The index of the public spend key", - "type": "integer", - "format": "uint64", - "minimum": 0 - }, - "seed": { - "description": "Seed used to derive the keys of the wallet", - "type": "array", - "items": { - "type": "integer", - "format": "uint8", - "minimum": 0 - }, - "maxItems": 64, - "minItems": 64 - } - } - }, "ExecuteArgs": { "description": "The arguments of the execute function", "type": "object", @@ -326,6 +299,42 @@ } } }, + "PublicSpendKeysArgs": { + "description": "The arguments of the public_spend_keys function", + "type": "object", + "required": [ + "seed" + ], + "properties": { + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + } + } + }, + "PublicSpendKeysResponse": { + "description": "The response of the public_spend_keys function", + "type": "object", + "required": [ + "keys" + ], + "properties": { + "keys": { + "description": "The Base58 public spend keys of the wallet.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "ViewKeysArgs": { "description": "The arguments of the view_keys function", "type": "object", diff --git a/build.rs b/build.rs index 25950a0..f077072 100644 --- a/build.rs +++ b/build.rs @@ -41,7 +41,9 @@ fn main() { use alloc::vec::Vec; use alloc::string::String; -use serde::{Serialize, Deserialize};"#; +use serde::{Serialize, Deserialize}; + +"#; let contents = header.to_owned() + &contents; diff --git a/src/ffi.rs b/src/ffi.rs index 1dcd7ac..6fb57b6 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -60,36 +60,6 @@ pub fn seed(args: i32, len: i32) -> i64 { utils::compose(true, ptr, len) } -/// Computes the public spend key from the given seed/index pair. -/// -/// Expects as argument a fat pointer to a JSON string representing -/// [types::PublicSpendKeyArgs]. -/// -/// Will return a triplet (status, ptr, len) pointing to the Base58 -/// representation of the public spend key. -#[no_mangle] -pub fn public_spend_key(args: i32, len: i32) -> i64 { - let types::PublicSpendKeyArgs { idx, seed } = - match utils::take_args(args, len) { - Some(a) => a, - None => return utils::fail(), - }; - - let seed = match utils::sanitize_seed(seed) { - Some(s) => s, - None => return utils::fail(), - }; - - let psk = key::derive_psk(&seed, idx); - let psk = bs58::encode(psk.to_bytes()).into_string(); - - let ptr = psk.as_ptr() as u32; - let len = psk.len() as u32; - - mem::forget(psk); - utils::compose(true, ptr, len) -} - /// Computes the total balance of the given notes. /// /// Expects as argument a fat pointer to a JSON string representing @@ -344,6 +314,34 @@ pub fn filter_notes(args: i32, len: i32) -> i64 { utils::rkyv_into_ptr(notes) } +/// Returns a list of [PublicSpendKey] that belongs to this wallet. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::PublicSpendKeysArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to JSON string +/// representing [types::PublicSpendKeysResponse]. +#[no_mangle] +pub fn public_spend_keys(args: i32, len: i32) -> i64 { + let types::PublicSpendKeysArgs { seed } = match utils::take_args(args, len) + { + Some(a) => a, + None => return utils::fail(), + }; + + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; + + let keys = (0..=MAX_KEY) + .map(|idx| key::derive_psk(&seed, idx as u64)) + .map(|psk| bs58::encode(psk.to_bytes()).into_string()) + .collect(); + + utils::into_ptr(types::PublicSpendKeysResponse { keys }) +} + /// Returns a list of [ViewKey] that belongs to this wallet. /// /// Expects as argument a fat pointer to a JSON string representing diff --git a/src/types.rs b/src/types.rs index 40e8a6d..cdda46c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,9 +10,10 @@ #![allow(missing_docs)] -use alloc::string::String; use alloc::vec::Vec; -use serde::{Deserialize, Serialize}; +use alloc::string::String; +use serde::{Serialize, Deserialize}; + #[doc = " The arguments of the balance function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct BalanceArgs { @@ -116,14 +117,18 @@ pub enum OutputType { Transparent, Obfuscated, } -#[doc = " The arguments of the public_spend_key function"] +#[doc = " The arguments of the public_spend_keys function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] -pub struct PublicSpendKeyArgs { - #[doc = " The index of the public spend key"] - pub idx: u64, +pub struct PublicSpendKeysArgs { #[doc = " Seed used to derive the keys of the wallet"] pub seed: Vec, } +#[doc = " The response of the public_spend_keys function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct PublicSpendKeysResponse { + #[doc = " The Base58 public spend keys of the wallet."] + pub keys: Vec, +} #[doc = " The arguments of the seed function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] pub struct SeedArgs { diff --git a/tests/wallet.rs b/tests/wallet.rs index f47df35..7dac980 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -8,7 +8,7 @@ use dusk_bytes::Serializable; use dusk_pki::PublicSpendKey; -use dusk_wallet_core::{tx, types, utils, MAX_LEN, RNG_SEED}; +use dusk_wallet_core::{tx, types, utils, MAX_KEY, MAX_LEN, RNG_SEED}; use rusk_abi::ContractId; use serde::{Deserialize, Serialize}; use serde_json::json; @@ -45,29 +45,6 @@ fn balance_works() { assert_eq!(maximum, 359); } -#[test] -fn public_spend_key_works() { - let seed = [0xfa; RNG_SEED]; - - let mut wallet = Wallet::default(); - - let psk = wallet - .call( - "public_spend_key", - json!({ - "seed": seed.to_vec(), - "idx": 3 - }), - ) - .take_memory(); - - let psk = bs58::decode(psk).into_vec().unwrap(); - let mut psk_array = [0u8; PublicSpendKey::SIZE]; - - psk_array.copy_from_slice(&psk); - PublicSpendKey::from_bytes(&psk_array).unwrap(); -} - #[test] fn execute_works() { let seed = [0xfa; RNG_SEED]; @@ -76,16 +53,15 @@ fn execute_works() { let mut wallet = Wallet::default(); - let psk = wallet + let types::PublicSpendKeysResponse { keys } = wallet .call( - "public_spend_key", + "public_spend_keys", json!({ "seed": seed.to_vec(), - "idx":5 }), ) - .take_memory(); - let psk = String::from_utf8(psk).unwrap(); + .take_contents(); + let psk = &keys[0]; let mut contract = ContractId::uninitialized(); contract.as_bytes_mut().iter_mut().for_each(|b| *b = 0xfa); @@ -105,7 +81,7 @@ fn execute_works() { "openings": openings, "output": { "note_type": "Transparent", - "receiver": psk.clone(), + "receiver": psk, "ref_id": 15, "value": 10, }, @@ -183,6 +159,31 @@ fn filter_notes_works() { assert_eq!(notes, filtered); } +#[test] +fn public_spend_keys_works() { + let seed = [0xfa; RNG_SEED]; + + let mut wallet = Wallet::default(); + + let types::PublicSpendKeysResponse { keys } = wallet + .call( + "public_spend_keys", + json!({ + "seed": seed.to_vec(), + }), + ) + .take_contents(); + + for key in &keys { + let key = bs58::decode(key).into_vec().unwrap(); + let mut key_array = [0u8; PublicSpendKey::SIZE]; + key_array.copy_from_slice(&key); + PublicSpendKey::from_bytes(&key_array).unwrap(); + } + + assert_eq!(keys.len(), MAX_KEY + 1); +} + #[test] fn view_keys_works() { let seed = [0xfa; RNG_SEED]; From 6f5b25c95181b3cc9949ea6b35d2e1cb2d53f08f Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 02:42:48 +0200 Subject: [PATCH 15/29] fix cargo fmt --- build.rs | 4 ++-- src/types.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/build.rs b/build.rs index f077072..003556c 100644 --- a/build.rs +++ b/build.rs @@ -39,9 +39,9 @@ fn main() { #![allow(missing_docs)] -use alloc::vec::Vec; use alloc::string::String; -use serde::{Serialize, Deserialize}; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; "#; diff --git a/src/types.rs b/src/types.rs index cdda46c..f58ac56 100644 --- a/src/types.rs +++ b/src/types.rs @@ -10,9 +10,9 @@ #![allow(missing_docs)] -use alloc::vec::Vec; use alloc::string::String; -use serde::{Serialize, Deserialize}; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; #[doc = " The arguments of the balance function"] #[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] From b8130ab1c16b496bdf5278040bd5779cac428729 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 02:49:46 +0200 Subject: [PATCH 16/29] upd toolchain on ci --- .github/workflows/dusk_ci.yml | 6 +++--- rust-toolchain | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 014c6d4..e58b403 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -34,7 +34,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: nightly-2023-05-22 - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: @@ -48,7 +48,7 @@ jobs: fail-fast: false matrix: toolchain: - - nightly + - nightly-2023-05-22 target: [ wasm32-unknown-unknown ] runs-on: ubuntu-latest steps: @@ -111,7 +111,7 @@ jobs: fail-fast: false matrix: toolchain: - - nightly + - nightly-2023-05-22 os: - ubuntu-latest runs-on: ${{ matrix.os }} diff --git a/rust-toolchain b/rust-toolchain index c309192..bf867e0 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2023-05-22 +nightly From 7d81ec5ca20b197117251a543dec96131af9c0c6 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 02:56:42 +0200 Subject: [PATCH 17/29] run build manually on ci to satisfy build script --- .github/workflows/dusk_ci.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index e58b403..c6a78c1 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -64,8 +64,11 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - - run: rustup target add wasm32-unknown-unknown - - run: rustup component add rust-src + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + - run: make wasm - name: Run cargo check @@ -125,8 +128,11 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} - - run: rustup target add wasm32-unknown-unknown - - run: rustup component add rust-src + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + - run: make wasm - name: Run cargo check From 06bb50891ff5e83bea052944fa28aeb4d6556a73 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:18:31 +0200 Subject: [PATCH 18/29] upd build wasm ci --- .github/workflows/dusk_ci.yml | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index c6a78c1..952b077 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -64,24 +64,7 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - - name: Run cargo build - uses: actions-rs/cargo@v1 - with: - command: build - - - run: make wasm - - - name: Run cargo check - uses: actions-rs/cargo@v1 - with: - command: check - args: --all-targets - - - name: Build project - uses: actions-rs/cargo@v1 - with: - command: rustc - args: --release --target ${{ matrix.target }} -- -C link-args=-s + - run: make package - name: Set up node if: startsWith(github.ref, 'refs/tags/v') From ffc689fa195945f71fef54cac10e2101b4034921 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:21:45 +0200 Subject: [PATCH 19/29] add rustup stdlib for wasm ci --- .github/workflows/dusk_ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 952b077..7814fbc 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -64,6 +64,9 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} + - make Prepare stdlib + run: rustup component add rust-src --toolchain nightly-2023-05-22-x86_64-unknown-linux-gnu + - run: make package - name: Set up node From b62adddde24194fb1e7db396577072e51bd53794 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:22:51 +0200 Subject: [PATCH 20/29] fix ci --- .github/workflows/dusk_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 7814fbc..05e3fa2 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -64,7 +64,7 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - - make Prepare stdlib + - name: Prepare stdlib run: rustup component add rust-src --toolchain nightly-2023-05-22-x86_64-unknown-linux-gnu - run: make package From 75524f806b8bac95aeecff8d7b1b0f223f9a9656 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:24:27 +0200 Subject: [PATCH 21/29] fix ci wasm version --- .github/workflows/dusk_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 05e3fa2..bec5582 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -65,7 +65,7 @@ jobs: run: rustup target add ${{ matrix.target }} - name: Prepare stdlib - run: rustup component add rust-src --toolchain nightly-2023-05-22-x86_64-unknown-linux-gnu + run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu - run: make package From 42f384b099e307be59f30d1c4afea8f331810989 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:33:21 +0200 Subject: [PATCH 22/29] add ci fix for schemafy non-deterministic build --- .github/workflows/dusk_ci.yml | 15 +++++++-------- build.rs | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index bec5582..6fe1f73 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -41,7 +41,6 @@ jobs: command: fmt args: --all -- --check - build_wasm: name: Build WASM strategy: @@ -64,8 +63,10 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - - name: Prepare stdlib - run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build - run: make package @@ -119,7 +120,8 @@ jobs: with: command: build - - run: make wasm + - name: Add target WASM + run: rustup target add wasm32-unknown-unknown - name: Run cargo check uses: actions-rs/cargo@v1 @@ -127,10 +129,7 @@ jobs: command: check args: --all-targets - - name: Test project - uses: actions-rs/cargo@v1 - with: - command: test + - run: make test # - name: Install kcov # if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} diff --git a/build.rs b/build.rs index 003556c..3e657d0 100644 --- a/build.rs +++ b/build.rs @@ -25,6 +25,7 @@ fn main() { // some limitations of schemafy will not allow it to parse the correct // integer type as they incorrectly fallback any integer to `i64` let contents = contents.replace("Vec", "Vec"); + let contents = contents.replace("Vec", "Vec"); let contents = contents.replace("i64", "u64"); let header = r#"// This Source Code Form is subject to the terms of the Mozilla Public From 3e864e37fdadbab37e880f9b0824d22cdd15b038 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:36:54 +0200 Subject: [PATCH 23/29] change order of schemafy fix --- build.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.rs b/build.rs index 3e657d0..976e21d 100644 --- a/build.rs +++ b/build.rs @@ -25,8 +25,8 @@ fn main() { // some limitations of schemafy will not allow it to parse the correct // integer type as they incorrectly fallback any integer to `i64` let contents = contents.replace("Vec", "Vec"); - let contents = contents.replace("Vec", "Vec"); let contents = contents.replace("i64", "u64"); + let contents = contents.replace("Vec", "Vec"); let header = r#"// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this From bc627e5f7dca0f910805b92afb946bc6493b083d Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:44:39 +0200 Subject: [PATCH 24/29] remove build script from ci --- .gitignore | 2 +- assets/dusk-wallet-core-0.21.0.wasm | Bin 0 -> 303327 bytes build.rs | 7 ++++++- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 assets/dusk-wallet-core-0.21.0.wasm diff --git a/.gitignore b/.gitignore index 6680449..e345084 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ target/ Cargo.lock **/*.rs.bk -*.wasm +mod.wasm diff --git a/assets/dusk-wallet-core-0.21.0.wasm b/assets/dusk-wallet-core-0.21.0.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b00b491a4b89e92ae51e81cf73545853aa2bdd8d GIT binary patch literal 303327 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 m1klqh_o@R;A(KMZ`oq5t(VxEbzF< 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

mWmHeE>tD4Bu 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 G+J68xJt|TF literal 0 HcmV?d00001 diff --git a/build.rs b/build.rs index 976e21d..5738436 100644 --- a/build.rs +++ b/build.rs @@ -4,10 +4,15 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use std::fs; +use std::{fs, env}; use std::path::PathBuf; fn main() { + if env::var("GITHUB_ACTIONS").is_ok() { + // CI doesn't build with schemafy deterministically + return (); + } + println!("cargo:rerun-if-changed=assets/schema.json"); let schema = PathBuf::from("assets/schema.json").canonicalize().unwrap(); From 232a58afb21f8a0fb40b9110208427d75eee1918 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:50:58 +0200 Subject: [PATCH 25/29] explicitly build wasm for tests in ci --- .github/workflows/dusk_ci.yml | 10 +++++----- build.rs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 6fe1f73..4f40f32 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -68,6 +68,9 @@ jobs: with: command: build + - name: Add rust-src + run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + - run: make package - name: Set up node @@ -115,11 +118,6 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} - - name: Run cargo build - uses: actions-rs/cargo@v1 - with: - command: build - - name: Add target WASM run: rustup target add wasm32-unknown-unknown @@ -129,6 +127,8 @@ jobs: command: check args: --all-targets + - run: make wasm + - run: make test # - name: Install kcov diff --git a/build.rs b/build.rs index 5738436..6651056 100644 --- a/build.rs +++ b/build.rs @@ -4,8 +4,8 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use std::{fs, env}; use std::path::PathBuf; +use std::{env, fs}; fn main() { if env::var("GITHUB_ACTIONS").is_ok() { From 58e8115f41061f0717fedde38796a66739f7f596 Mon Sep 17 00:00:00 2001 From: Artifex Date: Sun, 27 Aug 2023 23:58:31 +0200 Subject: [PATCH 26/29] reord ci --- .github/workflows/dusk_ci.yml | 16 ++++++++-------- Makefile | 2 ++ assets/dusk_wallet_core.wasm | Bin 0 -> 388808 bytes tests/wallet.rs | 4 +--- 4 files changed, 11 insertions(+), 11 deletions(-) create mode 100755 assets/dusk_wallet_core.wasm diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 4f40f32..d1ed1b3 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -71,6 +71,12 @@ jobs: - name: Add rust-src run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + - name: Install Binaryen + if: startsWith(github.ref, 'refs/tags/v') + run: > + wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-linux.tar.gz && + tar -xvf binaryen-version_105-x86_64-linux.tar.gz -C ~/.local --strip-components 1 + - run: make package - name: Set up node @@ -80,12 +86,6 @@ jobs: node-version: 16 registry-url: https://npm.pkg.github.com - - name: Install Binaryen - if: startsWith(github.ref, 'refs/tags/v') - run: > - wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-linux.tar.gz && - tar -xvf binaryen-version_105-x86_64-linux.tar.gz -C ~/.local --strip-components 1 - - name: Publish package if: startsWith(github.ref, 'refs/tags/v') # Move the compiled package to the root for better paths in the npm module. @@ -121,14 +121,14 @@ jobs: - name: Add target WASM run: rustup target add wasm32-unknown-unknown + - run: make wasm + - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check args: --all-targets - - run: make wasm - - run: make test # - name: Install kcov diff --git a/Makefile b/Makefile index ba3d2d1..a5b32d8 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,8 @@ wasm: ## Build the WASM files --color=always \ -Z build-std=core,alloc,panic_abort \ -Z build-std-features=panic_immediate_abort + @cp target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ + assets/ package: wasm ## Prepare the WASM package @wasm-opt -O3 $(WASM) -o $(NPM_WASM) diff --git a/assets/dusk_wallet_core.wasm b/assets/dusk_wallet_core.wasm new file mode 100755 index 0000000000000000000000000000000000000000..55e021186ad2ca0aead8198cf32bf0f56d3c0ddc GIT binary patch literal 388808 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 wZ&oIu<(7Dl8u)q>^$(UK8!KpK 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 AQUCw| literal 0 HcmV?d00001 diff --git a/tests/wallet.rs b/tests/wallet.rs index 7dac980..cdce6b9 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -430,9 +430,7 @@ impl Wallet { impl Default for Wallet { fn default() -> Self { - const WALLET: &[u8] = include_bytes!( - "../target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm" - ); + const WALLET: &[u8] = include_bytes!("../assets/dusk_wallet_core.wasm"); let mut store = Store::default(); let module = From 9f27a7341e0cf382161532f615f9590774b63692 Mon Sep 17 00:00:00 2001 From: Artifex Date: Mon, 28 Aug 2023 00:02:53 +0200 Subject: [PATCH 27/29] run ci build wasm only if going to publish --- .github/workflows/dusk_ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index d1ed1b3..5ac45fd 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -43,6 +43,7 @@ jobs: build_wasm: name: Build WASM + if: startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: @@ -72,7 +73,6 @@ jobs: run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu - name: Install Binaryen - if: startsWith(github.ref, 'refs/tags/v') run: > wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-linux.tar.gz && tar -xvf binaryen-version_105-x86_64-linux.tar.gz -C ~/.local --strip-components 1 @@ -80,14 +80,12 @@ jobs: - run: make package - name: Set up node - if: startsWith(github.ref, 'refs/tags/v') uses: actions/setup-node@v2 with: node-version: 16 registry-url: https://npm.pkg.github.com - name: Publish package - if: startsWith(github.ref, 'refs/tags/v') # Move the compiled package to the root for better paths in the npm module. # We also automatically populate the version with the given tag. run: > @@ -121,6 +119,9 @@ jobs: - name: Add target WASM run: rustup target add wasm32-unknown-unknown + - name: Add rust-src + run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + - run: make wasm - name: Run cargo check From 4617befb0417da8c4ec0c45c893f3f72677ce1a2 Mon Sep 17 00:00:00 2001 From: Artifex Date: Mon, 28 Aug 2023 00:08:41 +0200 Subject: [PATCH 28/29] reactivate kcov --- .github/workflows/dusk_ci.yml | 50 ++++++++++++++++------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 5ac45fd..8669145 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -132,30 +132,26 @@ jobs: - run: make test -# - name: Install kcov -# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} -# run: sudo apt install -y kcov -# -# - name: Build test executable -# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} -# uses: actions-rs/cargo@v1 -# env: -# RUSTFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' -# RUSTDOCFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' -# with: -# command: test -# args: --no-run -# -# - name: Test with kcov -# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} -# # Find every executable resulting from building the tests and run each -# # one of them with kcov. This ensures all the code we cover is measured. -# run: > -# find target/debug/deps -type f -executable ! -name "*.*" | -# xargs -n1 kcov --exclude-pattern=tests/,/.cargo,/usr/lib --verify target/cov -# -# - name: Upload coverage -# if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} -# uses: codecov/codecov-action@v1.0.2 -# with: -# token: ${{secrets.CODECOV_TOKEN}} + - name: Install kcov + run: sudo apt install -y kcov + + - name: Build test executable + uses: actions-rs/cargo@v1 + env: + RUSTFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' + RUSTDOCFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' + with: + command: test + args: --no-run + + - name: Test with kcov + # Find every executable resulting from building the tests and run each + # one of them with kcov. This ensures all the code we cover is measured. + run: > + find target/debug/deps -type f -executable ! -name "*.*" | + xargs -n1 kcov --exclude-pattern=tests/,/.cargo,/usr/lib --verify target/cov + + - name: Upload coverage + uses: codecov/codecov-action@v1.0.2 + with: + token: ${{secrets.CODECOV_TOKEN}} From aeacba65e35accd55e83b7f602c5b539e2a3ef3d Mon Sep 17 00:00:00 2001 From: Artifex Date: Mon, 28 Aug 2023 00:18:29 +0200 Subject: [PATCH 29/29] run coverage only from main --- .github/workflows/dusk_ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 8669145..8b208de 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -133,9 +133,11 @@ jobs: - run: make test - name: Install kcov + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} run: sudo apt install -y kcov - name: Build test executable + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} uses: actions-rs/cargo@v1 env: RUSTFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' @@ -145,6 +147,7 @@ jobs: args: --no-run - name: Test with kcov + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} # Find every executable resulting from building the tests and run each # one of them with kcov. This ensures all the code we cover is measured. run: > @@ -152,6 +155,7 @@ jobs: xargs -n1 kcov --exclude-pattern=tests/,/.cargo,/usr/lib --verify target/cov - name: Upload coverage + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} uses: codecov/codecov-action@v1.0.2 with: token: ${{secrets.CODECOV_TOKEN}}