From dac4f1c1c62950725e1534b49569fd5085ef7351 Mon Sep 17 00:00:00 2001 From: Artifex Date: Thu, 24 Aug 2023 00:08:37 +0200 Subject: [PATCH] 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, + } + } }