diff --git a/.github/workflows/dusk_ci.yml b/.github/workflows/dusk_ci.yml index 82853d5..8b208de 100644 --- a/.github/workflows/dusk_ci.yml +++ b/.github/workflows/dusk_ci.yml @@ -9,6 +9,7 @@ on: name: Dusk CI jobs: + analyze: name: Dusk Analyzer runs-on: ubuntu-latest @@ -33,7 +34,7 @@ jobs: - uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: nightly + toolchain: nightly-2023-05-22 - run: rustup component add rustfmt - uses: actions-rs/cargo@v1 with: @@ -42,11 +43,12 @@ jobs: build_wasm: name: Build WASM + if: startsWith(github.ref, 'refs/tags/v') strategy: fail-fast: false matrix: toolchain: - - nightly + - nightly-2023-05-22 target: [ wasm32-unknown-unknown ] runs-on: ubuntu-latest steps: @@ -62,49 +64,45 @@ jobs: - name: Add target run: rustup target add ${{ matrix.target }} - - name: Run cargo check + - name: Run cargo build uses: actions-rs/cargo@v1 with: - command: check - args: --all-targets + command: build - - name: Build project - uses: actions-rs/cargo@v1 - with: - command: rustc - args: --release --target ${{ matrix.target }} -- -C link-args=-s + - name: Add rust-src + run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + + - name: Install Binaryen + run: > + wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-linux.tar.gz && + tar -xvf binaryen-version_105-x86_64-linux.tar.gz -C ~/.local --strip-components 1 + + - run: make package - name: Set up node - if: startsWith(github.ref, 'refs/tags/v') uses: actions/setup-node@v2 with: node-version: 16 registry-url: https://npm.pkg.github.com - - name: Install Binaryen - if: startsWith(github.ref, 'refs/tags/v') - run: > - wget https://github.com/WebAssembly/binaryen/releases/download/version_105/binaryen-version_105-x86_64-linux.tar.gz && - tar -xvf binaryen-version_105-x86_64-linux.tar.gz -C ~/.local --strip-components 1 - - name: Publish package - if: startsWith(github.ref, 'refs/tags/v') # Move the compiled package to the root for better paths in the npm module. # 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: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build_and_test: name: Test with all features strategy: fail-fast: false matrix: toolchain: - - nightly + - nightly-2023-05-22 os: - ubuntu-latest runs-on: ${{ matrix.os }} @@ -118,24 +116,28 @@ jobs: profile: minimal toolchain: ${{ matrix.toolchain }} + - name: Add target WASM + run: rustup target add wasm32-unknown-unknown + + - name: Add rust-src + run: rustup component add rust-src --toolchain nightly-x86_64-unknown-linux-gnu + + - run: make wasm + - name: Run cargo check uses: actions-rs/cargo@v1 with: command: check args: --all-targets - - name: Test project - if: ${{ matrix.os != 'ubuntu-latest' || matrix.toolchain != 'nightly' }} - uses: actions-rs/cargo@v1 - with: - command: test + - run: make test - name: Install kcov - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} run: sudo apt install -y kcov - name: Build test executable - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} uses: actions-rs/cargo@v1 env: RUSTFLAGS: '-Cdebuginfo=2 -Cinline-threshold=0 -Clink-dead-code' @@ -145,7 +147,7 @@ jobs: args: --no-run - name: Test with kcov - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} # Find every executable resulting from building the tests and run each # one of them with kcov. This ensures all the code we cover is measured. run: > @@ -153,7 +155,7 @@ jobs: xargs -n1 kcov --exclude-pattern=tests/,/.cargo,/usr/lib --verify target/cov - name: Upload coverage - if: ${{ matrix.os == 'ubuntu-latest' && matrix.toolchain == 'nightly' }} + if: ${{ matrix.os == 'ubuntu-latest' && github.ref == 'refs/heads/main' }} uses: codecov/codecov-action@v1.0.2 with: token: ${{secrets.CODECOV_TOKEN}} diff --git a/.gitignore b/.gitignore index 6680449..e345084 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ target/ Cargo.lock **/*.rs.bk -*.wasm +mod.wasm diff --git a/Cargo.toml b/Cargo.toml index 48cedd9..aad3afe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,39 @@ [package] name = "dusk-wallet-core" -version = "0.20.0-piecrust.0.6" +version = "0.21.0" edition = "2021" description = "The core functionality of the Dusk wallet" license = "MPL-2.0" +[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 } +bytecheck = { version = "0.6", default-features = false } +bs58 = { version = "0.5", default-features = false, features = ["alloc", "cb58"] } +dusk-bls12_381-sign = { version = "0.4", default-features = false } dusk-bytes = "^0.1" -dusk-schnorr = { version = "0.13", default-features = false } dusk-jubjub = { version = "0.12", default-features = false } -dusk-poseidon = { version = "0.30", default-features = false } +dusk-pki = { version = "0.12", default-features = false, features = ["rkyv-impl"] } +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_chacha = { version = "^0.3", default-features = false } +rand_core = "^0.6" +rkyv = { version = "^0.7", default-features = false, features = ["size_32"] } +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +sha2 = { version = "^0.10", default-features = false } + +[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" +wasmer = "=3.1" -[lib] -crate-type = ["cdylib", "rlib"] +[build-dependencies] +schemafy_lib = "0.6" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a5b32d8 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +PROJECT := $(shell sed -n '0,/name\s*=\s*"\(.*\)"/s/name\s*=\s*"\(.*\)"/\1/p' Cargo.toml) +VERSION := $(shell sed -n '0,/version\s*=\s*"\(.*\)"/s/version\s*=\s*"\(.*\)"/\1/p' Cargo.toml) +FLAGS := RUSTFLAGS="$(RUSTFLAGS) --remap-path-prefix $(HOME)= -C link-args=-zstack-size=65536" +WASM := "target/wasm32-unknown-unknown/release/$(shell sed -n '0,/name\s*=\s*"\(.*\)"/s/name\s*=\s*"\(.*\)"/\1/p' Cargo.toml | sed 's/-/_/g').wasm" +NPM_WASM := "mod.wasm" +PACKAGE := "assets/$(PROJECT)-$(VERSION).wasm" + +help: ## Display this help screen + @grep -h \ + -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ + 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 + @$(FLAGS) cargo build --release \ + --target wasm32-unknown-unknown \ + --color=always \ + -Z build-std=core,alloc,panic_abort \ + -Z build-std-features=panic_immediate_abort + @cp target/wasm32-unknown-unknown/release/dusk_wallet_core.wasm \ + assets/ + +package: wasm ## Prepare the WASM package + @wasm-opt -O3 $(WASM) -o $(NPM_WASM) + @cp $(NPM_WASM) $(PACKAGE) + @echo "Package created: $(PACKAGE)" + +.PHONY: test wasm package help diff --git a/README.md b/README.md index 0d5cb8b..ebda641 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,46 @@ [![codecov](https://codecov.io/gh/dusk-network/wallet-core/branch/main/graph/badge.svg?token=9W3J09AWZG)](https://codecov.io/gh/dusk-network/wallet-core) [![documentation](https://img.shields.io/badge/docs-wallet-blue?logo=rust)](https://docs.rs/dusk-wallet-core/) -A library for generating and dealing with transactions. +A WASM library to provide business logic for Dusk wallet implementations. + +Check the available methods under the [FFI](src/ffi.rs) module. + +Every function expects a fat pointer to its arguments already allocated to the WASM memory. For the arguments definition, check the [JSON Schema](assets/schema.json). It will consume this pointer region and free it after execution. The return of the function will also be in accordance to the schema, and the user will have to free the memory himself after fetching the data. + +For maximum compatibility, every WASM function returns a `i64` with the status of the operation and an embedded pointer. The structure of the bytes in big-endian is as follows: + +[(pointer) x 4bytes (length) x 3bytes (status) x 1bit] + +The pointer will be a maximum `u32` number, and the length a `u24` number. The status of the operation is the least significant bit of the number, and will be `0` if the operation is successful. + +Here is an algorithm to split the result into meaningful parts: + +```rust,ignore +let ptr = (result >> 32) as u64; +let len = ((result << 32) >> 48) as u64; +let success = ((result << 63) >> 63) == 0; +``` + +For an example usage, check the [wallet-cli](https://github.com/dusk-network/wallet-cli) implementation that consumes this library. + +## Requirements + +- [Rust 1.71.0](https://www.rust-lang.org/) +- [target.wasm32-unknown-unknown](https://github.com/rustwasm/) +- [binaryen](https://github.com/WebAssembly/binaryen) to generate packages ## Build -To build and test the crate: +To build a distributable package: -```shell -cargo b -cargo t --all-features +```sh +make package ``` -To build the WASM module: +## Test + +To run the tests, there is an automated Makefile script -```shell -cargo b --release --target wasm32-unknown-unknown +```sh +make test ``` diff --git a/assets/dusk-wallet-core-0.21.0.wasm b/assets/dusk-wallet-core-0.21.0.wasm new file mode 100644 index 0000000..b00b491 Binary files /dev/null and b/assets/dusk-wallet-core-0.21.0.wasm differ diff --git a/assets/dusk_wallet_core.wasm b/assets/dusk_wallet_core.wasm new file mode 100755 index 0000000..55e0211 Binary files /dev/null and b/assets/dusk_wallet_core.wasm differ diff --git a/assets/schema.json b/assets/schema.json new file mode 100644 index 0000000..7c418f1 --- /dev/null +++ b/assets/schema.json @@ -0,0 +1,389 @@ +{ + "$id": "https://json.schemastore.org/base.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "SeedArgs": { + "description": "The arguments of the seed function", + "type": "object", + "required": [ + "passphrase" + ], + "properties": { + "passphrase": { + "description": "An arbitrary sequence of bytes used to generate a secure seed", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "BalanceArgs": { + "description": "The arguments of the balance function", + "type": "object", + "required": [ + "notes", + "seed" + ], + "properties": { + "notes": { + "description": "A rkyv serialized [Vec]; all notes should have their keys derived from `seed`", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + } + } + }, + "BalanceResponse": { + "description": "The response of the balance function", + "type": "object", + "required": [ + "maximum", + "value" + ], + "properties": { + "maximum": { + "description": "Maximum value per transaction", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "value": { + "description": "Total computed balance", + "type": "integer", + "format": "uint64", + "minimum": 0 + } + } + }, + "ExecuteCall": { + "description": "A call to a contract method", + "type": "object", + "required": [ + "contract", + "method", + "payload" + ], + "properties": { + "contract": { + "description": "The id of the contract to call in Base58 format", + "type": "string" + }, + "method": { + "description": "The name of the method to be called", + "type": "string" + }, + "payload": { + "description": "The payload of the call", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "OutputType": { + "description": "A note type variant", + "type": "string", + "enum": [ + "Transparent", + "Obfuscated" + ] + }, + "ExecuteOutput": { + "description": "The output of a transfer", + "type": "object", + "required": [ + "note_type", + "value", + "receiver" + ], + "properties": { + "note_type": { + "description": "The type of the note", + "$ref": "#/definitions/OutputType" + }, + "value": { + "description": "The value of the output", + "type": "integer", + "format": "uint64", + "minimum": 1 + }, + "receiver": { + "description": "The address of the receiver in Base58 format", + "type": "string" + }, + "ref_id": { + "description": "A reference id to be appended to the output", + "type": "integer", + "format": "uint64", + "minimum": 1 + } + } + }, + "ExecuteArgs": { + "description": "The arguments of the execute function", + "type": "object", + "required": [ + "gas_limit", + "gas_price", + "inputs", + "openings", + "refund", + "rng_seed", + "seed" + ], + "properties": { + "call": { + "description": "A call to a contract method", + "$ref": "#/definitions/ExecuteCall" + }, + "crossover": { + "description": "The [phoenix_core::Crossover] value", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "gas_limit": { + "description": "The gas limit of the transaction", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "gas_price": { + "description": "The gas price per unit for the transaction", + "type": "integer", + "format": "uint64", + "minimum": 0 + }, + "inputs": { + "description": "A rkyv serialized [Vec] to be used as inputs", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "openings": { + "description": "A rkyv serialized [Vec] to open the inputs to a Merkle root", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "output": { + "description": "The transfer output note", + "$ref": "#/definitions/ExecuteOutput" + }, + "refund": { + "description": "The refund addressin Base58 format", + "type": "string" + }, + "rng_seed": { + "description": "Seed used to derive the entropy for the notes", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + } + } + }, + "ExecuteResponse": { + "description": "The response of the execute function", + "type": "object", + "required": [ + "tx", + "unspent" + ], + "properties": { + "tx": { + "description": "A rkyv serialized [crate::tx::UnspentTransaction]", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "unspent": { + "description": "A rkyv serialized [Vec] containing the notes that weren't used", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "MergeNotesArgs": { + "description": "The arguments of the merge_notes function", + "type": "object", + "required": [ + "notes" + ], + "properties": { + "notes": { + "description": "All serialized list of notes to be merged", + "type": "array", + "items": { + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + } + }, + "FilterNotesArgs": { + "description": "The arguments of the filter_notes function", + "type": "object", + "required": [ + "flags", + "notes" + ], + "properties": { + "flags": { + "description": "Boolean flags to be negative filtered", + "type": "array", + "items": { + "type": "boolean" + } + }, + "notes": { + "description": "A rkyv serialized [Vec] to be filtered", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + } + } + }, + "PublicSpendKeysArgs": { + "description": "The arguments of the public_spend_keys function", + "type": "object", + "required": [ + "seed" + ], + "properties": { + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + } + } + }, + "PublicSpendKeysResponse": { + "description": "The response of the public_spend_keys function", + "type": "object", + "required": [ + "keys" + ], + "properties": { + "keys": { + "description": "The Base58 public spend keys of the wallet.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "ViewKeysArgs": { + "description": "The arguments of the view_keys function", + "type": "object", + "required": [ + "seed" + ], + "properties": { + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + } + } + }, + "NullifiersArgs": { + "description": "The arguments of the nullifiers function", + "type": "object", + "required": [ + "notes", + "seed" + ], + "properties": { + "notes": { + "description": "A rkyv serialized [Vec] to have nullifiers generated", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + } + }, + "seed": { + "description": "Seed used to derive the keys of the wallet", + "type": "array", + "items": { + "type": "integer", + "format": "uint8", + "minimum": 0 + }, + "maxItems": 64, + "minItems": 64 + } + } + } + } +} 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/build.rs b/build.rs new file mode 100644 index 0000000..6651056 --- /dev/null +++ b/build.rs @@ -0,0 +1,57 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +use std::path::PathBuf; +use std::{env, fs}; + +fn main() { + if env::var("GITHUB_ACTIONS").is_ok() { + // CI doesn't build with schemafy deterministically + return (); + } + + println!("cargo:rerun-if-changed=assets/schema.json"); + + let schema = PathBuf::from("assets/schema.json").canonicalize().unwrap(); + let types = PathBuf::from("src/types.rs"); + + schemafy_lib::Generator::builder() + .with_input_file(&schema) + .build() + .generate_to_file(&types) + .unwrap(); + + let types = types.canonicalize().unwrap(); + let contents = fs::read_to_string(&types).unwrap(); + + // some limitations of schemafy will not allow it to parse the correct + // integer type as they incorrectly fallback any integer to `i64` + let contents = contents.replace("Vec", "Vec"); + let contents = contents.replace("i64", "u64"); + let contents = contents.replace("Vec", "Vec"); + + let header = r#"// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Arguments and responses to the module requests + +// THIS FILE IS AUTO GENERATED!! + +#![allow(missing_docs)] + +use alloc::string::String; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +"#; + + let contents = header.to_owned() + &contents; + + fs::write(&types, contents).unwrap(); +} diff --git a/rust-toolchain b/rust-toolchain index 67946b1..bf867e0 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -nightly-2023-01-05 +nightly diff --git a/src/ffi.rs b/src/ffi.rs index 9eeaf75..6fb57b6 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -4,686 +4,419 @@ // // 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 crate::{ - BalanceInfo, EnrichedNote, Error, ProverClient, StakeInfo, StateClient, - Store, Transaction, Wallet, POSEIDON_TREE_DEPTH, -}; - -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(); - } - } - }; -} +use dusk_bytes::Serializable; +use phoenix_core::Note; +use sha2::{Digest, Sha512}; -type FfiWallet = Wallet; -const WALLET: FfiWallet = - Wallet::new(FfiStore, FfiStateClient, FfiProverClient); +use crate::{key, tx, types, utils, MAX_KEY, MAX_LEN}; -/// Allocates memory with a given size. +/// Allocates a buffer of `len` bytes on the WASM memory. #[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 +pub fn malloc(len: i32) -> i32 { + let bytes = vec![0u8; len as usize]; + let ptr = bytes.as_ptr(); + mem::forget(bytes); + ptr as i32 } -/// Free memory pointed to by the given `ptr`, and the given `cap`acity. +/// Frees a previously allocated buffer 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); -} - -/// Get the public spend key with the given index. -#[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 +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); + } } -/// Execute a generic contract call +/// Computes a secure seed from the given passphrase. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::SeedArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the seed. #[no_mangle] -pub 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, - ); +pub fn seed(args: i32, len: i32) -> i64 { + let types::SeedArgs { passphrase } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; - let refund = unwrap_or_bail!(PublicSpendKey::from_bytes(&*refund)); + let mut hash = Sha512::new(); - unwrap_or_bail!(WALLET.execute( - &mut FfiRng, - contract_id, - call_name, - call_data, - *sender_index, - &refund, - *gas_price, - *gas_limit - )); + hash.update(passphrase); + hash.update(b"SEED"); - 0 -} + let seed = hash.finalize().to_vec(); + let ptr = seed.as_ptr() as u32; + let len = seed.len() as u32; -/// 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 + mem::forget(seed); + utils::compose(true, ptr, len) } -/// Creates a stake transaction. +/// Computes the total balance of the given notes. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::BalanceArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to JSON string +/// representing [types::BalanceResult]. #[no_mangle] -pub 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 -} +pub fn balance(args: i32, len: i32) -> i64 { + let types::BalanceArgs { notes, seed } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; -/// 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 -} + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; -struct FfiStore; + let notes: Vec = match rkyv::from_bytes(¬es) { + Ok(n) => utils::sanitize_notes(n), + Err(_) => return utils::fail(), + }; -impl Store for FfiStore { - type Error = u8; + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut values = Vec::with_capacity(notes.len()); + let mut keys_len = 0; + let mut sum = 0u64; + + '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; + } - 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); + if let Ok(v) = note.value(Some(&keys[idx])) { + values.push(v); + sum = sum.saturating_add(v); + continue 'outer; } } - Ok(seed) + + return utils::fail(); } + + // the top 4 notes are the maximum value a transaction can have, given the + // circuit accepts up to 4 inputs + values.sort_by(|a, b| b.cmp(a)); + let maximum = values.iter().take(4).sum::(); + + utils::into_ptr(types::BalanceResponse { + maximum, + value: sum, + }) } -const STCT_INPUT_SIZE: usize = Fee::SIZE - + Crossover::SIZE - + u64::SIZE - + JubJubScalar::SIZE - + BlsScalar::SIZE - + Signature::SIZE; +/// Computes a serialized unproven transaction from the given arguments. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::ExecuteArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to JSON string +/// representing [types::ExecuteResponse]. +#[no_mangle] +pub fn execute(args: i32, len: i32) -> i64 { + let types::ExecuteArgs { + call, + crossover, + gas_limit, + gas_price, + inputs, + openings, + output, + refund, + rng_seed, + seed, + } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; -const WFCT_INPUT_SIZE: usize = - JubJubAffine::SIZE + u64::SIZE + JubJubScalar::SIZE; + let inputs: Vec = match rkyv::from_bytes(&inputs) { + Ok(n) => utils::sanitize_notes(n), + Err(_) => return utils::fail(), + }; -struct FfiStateClient; + let openings: Vec = match rkyv::from_bytes(&openings) { + Ok(n) => n, + Err(_) => return utils::fail(), + }; -impl StateClient for FfiStateClient { - type Error = u8; + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; - fn fetch_notes( - &self, - vk: &ViewKey, - ) -> Result, Self::Error> { - let mut notes_ptr = ptr::null_mut(); - let mut notes_len = 0; + let rng_seed = match utils::sanitize_seed(rng_seed) { + Some(s) => s, + None => return utils::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 value = output.as_ref().map(|o| o.value).unwrap_or(0); + let total_output = gas_limit + .saturating_mul(gas_price) + .saturating_add(value) + .saturating_add(crossover.unwrap_or_default()); + + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + 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; } - // 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, - ) - }; + if let Ok(value) = input.value(Some(&keys[idx])) { + let opening = match openings.next() { + Some(o) => o, + None => return utils::fail(), + }; - 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)); + full_inputs.push((input, opening, value, idx)); + continue 'outer; + } } - Ok(notes) + return utils::fail(); } - 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); - } - } - let scalar = BlsScalar::from_bytes(&scalar_buf).map_err( - Error::::from, - )?; + // optimizes the inputs given the total amount + let (unspent, inputs) = match utils::knapsack(full_inputs, total_output) { + Some(k) => k, + None => return utils::fail(), + }; - Ok(scalar) + let inputs: Vec<_> = inputs + .into_iter() + .map(|(note, opening, value, idx)| tx::PreInput { + note, + opening, + value, + ssk: &keys_ssk[idx], + }) + .collect(); + + let total_input: u64 = inputs.iter().map(|i| i.value).sum(); + let total_refund = total_input.saturating_sub(total_output); + + let mut outputs = Vec::with_capacity(2); + if let Some(o) = output { + outputs.push(o); + } + if total_refund > 0 { + outputs.push(types::ExecuteOutput { + note_type: types::OutputType::Obfuscated, + receiver: refund.clone(), + ref_id: None, + value: total_refund, + }); } - 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![]); - } + let rng = &mut utils::rng(&rng_seed); + let tx = tx::UnprovenTransaction::new( + rng, inputs, outputs, refund, gas_limit, gas_price, crossover, call, + ); + let tx = match tx { + Some(t) => t, + None => return utils::fail(), + }; - let mut writer = &mut nullifiers_buf[..]; + let unspent = match rkyv::to_bytes::<_, MAX_LEN>(&unspent).ok() { + Some(t) => t.into_vec(), + None => return utils::fail(), + }; - for nullifier in nullifiers { - writer.write(&nullifier.to_bytes()).map_err( - Error::::from, - )?; - } + let tx = match rkyv::to_bytes::<_, MAX_LEN>(&tx).ok() { + Some(t) => t.into_vec(), + None => return utils::fail(), + }; - 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); - } - }; + utils::into_ptr(types::ExecuteResponse { tx, unspent }) +} - let mut existing_nullifiers = - Vec::with_capacity(existing_nullifiers_len as usize); +/// Merges many lists of serialized notes into a unique, sanitized set. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::MergeNotesArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. +#[no_mangle] +pub fn merge_notes(args: i32, len: i32) -> i64 { + let types::MergeNotesArgs { notes } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; - let mut reader = &existing_nullifiers_buf[..]; - for _ in 0..existing_nullifiers_len { - existing_nullifiers.push( - BlsScalar::from_reader(&mut reader).map_err( - Error::::from, - )?, - ); + let mut list = Vec::with_capacity(10); + for notes in notes { + if !notes.is_empty() { + match rkyv::from_bytes::>(¬es) { + Ok(n) => list.extend(n), + Err(_) => return utils::fail(), + }; } - - 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); - } - } - - let branch = rkyv::from_bytes(&opening_buf[..opening_len as usize]) - .map_err( - Error::::from, - )?; + let notes = utils::sanitize_notes(list); - Ok(branch) - } + utils::rkyv_into_ptr(notes) +} - fn fetch_stake(&self, pk: &PublicKey) -> Result { - let pk = pk.to_bytes(); - let mut stake_buf = [0u8; StakeInfo::SIZE]; +/// Filters a list of notes from a list of negative flags. The flags that are +/// `true` will represent a note that must be removed from the set. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::FilterNotesArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. +#[no_mangle] +pub fn filter_notes(args: i32, len: i32) -> i64 { + let types::FilterNotesArgs { flags, notes } = + match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; - unsafe { - let r = fetch_stake(&pk, &mut stake_buf); - if r != 0 { - return Err(r); - } - } + let notes: Vec = match rkyv::from_bytes(¬es) { + Ok(n) => n, + Err(_) => return utils::fail(), + }; - let stake = StakeInfo::from_bytes(&stake_buf).map_err( - Error::::from, - )?; + let notes: Vec<_> = notes + .into_iter() + .zip(flags.into_iter()) + .filter_map(|(n, f)| (!f).then_some(n)) + .collect(); - Ok(stake) - } + let notes = utils::sanitize_notes(notes); + utils::rkyv_into_ptr(notes) } -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); - } - } +/// Returns a list of [PublicSpendKey] that belongs to this wallet. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::PublicSpendKeysArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to JSON string +/// representing [types::PublicSpendKeysResponse]. +#[no_mangle] +pub fn public_spend_keys(args: i32, len: i32) -> i64 { + let types::PublicSpendKeysArgs { seed } = match utils::take_args(args, len) + { + Some(a) => a, + None => return utils::fail(), + }; + + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; - let transaction = Transaction::from_slice(&tx_buf[..tx_len as usize]) - .map_err( - Error::::from, - )?; + let keys = (0..=MAX_KEY) + .map(|idx| key::derive_psk(&seed, idx as u64)) + .map(|psk| bs58::encode(psk.to_bytes()).into_string()) + .collect(); - Ok(transaction) - } + utils::into_ptr(types::PublicSpendKeysResponse { keys }) +} - 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 list of [ViewKey] that belongs to this wallet. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::ViewKeysArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. +#[no_mangle] +pub fn view_keys(args: i32, len: i32) -> i64 { + let types::ViewKeysArgs { seed } = match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; - let proof = Proof::from_bytes(&proof_buf).map_err( - Error::::from, - )?; - Ok(proof) - } + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; - 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); - } - } + let vks: Vec<_> = (0..=MAX_KEY) + .map(|idx| key::derive_vk(&seed, idx as u64)) + .collect(); - let proof = Proof::from_bytes(&proof_buf).map_err( - Error::::from, - )?; - Ok(proof) - } + utils::rkyv_into_ptr(vks) } -struct FfiRng; - -impl CryptoRng for FfiRng {} +/// Returns a list of [BlsScalar] nullifiers for the given [Vec] combined +/// with the keys of this wallet. +/// +/// Expects as argument a fat pointer to a JSON string representing +/// [types::NullifiersArgs]. +/// +/// Will return a triplet (status, ptr, len) pointing to the rkyv serialized +/// [Vec]. +#[no_mangle] +pub fn nullifiers(args: i32, len: i32) -> i64 { + let types::NullifiersArgs { notes, seed } = + match utils::take_args(args, len) { + Some(a) => a, + None => return utils::fail(), + }; -impl RngCore for FfiRng { - fn next_u32(&mut self) -> u32 { - next_u32_via_fill(self) - } + let notes: Vec = match rkyv::from_bytes(¬es) { + Ok(n) => n, + Err(_) => return utils::fail(), + }; - fn next_u64(&mut self) -> u64 { - next_u64_via_fill(self) - } + let seed = match utils::sanitize_seed(seed) { + Some(s) => s, + None => return utils::fail(), + }; - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.try_fill_bytes(dest).ok(); - } + let mut nullifiers = Vec::with_capacity(notes.len()); + let mut keys = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_ssk = unsafe { [mem::zeroed(); MAX_KEY + 1] }; + let mut keys_len = 0; + + 'outer: for note in notes { + // we iterate all the available keys until one can successfully + // decrypt the note. if any fails, returns false + for idx in 0..=MAX_KEY { + if keys_len == idx { + keys_ssk[idx] = key::derive_ssk(&seed, idx as u64); + keys[idx] = keys_ssk[idx].view_key(); + keys_len += 1; + } - 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)) - } + if keys[idx].owns(¬e) { + nullifiers.push(note.gen_nullifier(&keys_ssk[idx])); + continue 'outer; } } - } -} -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, - } + return utils::fail(); } + + utils::rkyv_into_ptr(nullifiers) } 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..d6fb81f --- /dev/null +++ b/src/key.rs @@ -0,0 +1,48 @@ +// 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, RNG_SEED}; + +use dusk_bls12_381_sign::SecretKey; +use dusk_pki::{PublicSpendKey, 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; 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; RNG_SEED], index: u64) -> SecretKey { + SecretKey::random(&mut utils::rng_with_index(seed, index)) +} + +/// Generates a public spend key from its seed and index. +/// +/// The secret spend key is derived from [derive_ssk], and then the key is +/// generated via [SecretSpendKey::public_spend_key]. +pub fn derive_psk(seed: &[u8; RNG_SEED], index: u64) -> PublicSpendKey { + derive_ssk(seed, index).public_spend_key() +} + +/// Generates a view key from its seed and index. +/// +/// The secret spend key is derived from [derive_ssk], and then the key is +/// generated via [SecretSpendKey::view_key]. +pub fn derive_vk(seed: &[u8; RNG_SEED], index: u64) -> ViewKey { + derive_ssk(seed, index).view_key() +} diff --git a/src/lib.rs b/src/lib.rs index 8269f65..df998c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,287 +4,27 @@ // // 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; -} - -/// 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. - 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 - } -} - -/// 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, -} - -impl From for StakeInfo { - fn from(data: StakeData) -> Self { - StakeInfo { - amount: data.amount, - reward: data.reward, - counter: data.counter, - } - } -} - -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, - }) - } +pub mod ffi; +pub mod key; +pub mod tx; +pub mod types; +pub mod utils; - /// 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[..]; +/// The maximum number of keys (inclusive) to derive when attempting to decrypt +/// a note. +pub const MAX_KEY: usize = 24; - let (value, eligibility) = self.amount.unwrap_or_default(); +/// The maximum allocated buffer for rkyv serialization. +pub const MAX_LEN: usize = rusk_abi::ARGBUF_LEN; - writer.write(&value.to_bytes()); - writer.write(&eligibility.to_bytes()); - writer.write(&self.reward.to_bytes()); - writer.write(&self.counter.to_bytes()); +/// Length of the seed of the generated rng. +pub const RNG_SEED: usize = 64; - buf - } -} +/// The length of the allocated response. +pub const RESPONSE_LEN: usize = 3 * i32::BITS as usize / 8; diff --git a/src/tx.rs b/src/tx.rs index 578c1ed..8c89930 100644 --- a/src/tx.rs +++ b/src/tx.rs @@ -4,221 +4,284 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -use crate::StateClient; +//! Unspent transaction definition. use alloc::string::String; use alloc::vec::Vec; +use core::mem; -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}; + +use crate::{types, utils}; + +/// 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(); + inputs: I, + outputs: O, + refund: String, + gas_limit: u64, + gas_price: u64, + crossover: Option, + 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 mut openings = Vec::with_capacity(inputs.len()); - for (note, _, _) in &inputs { - let opening = state.fetch_opening(note)?; - openings.push(opening); - } + let anchor = inputs.first().map(|i| i.opening.root().hash)?; + let refund = utils::bs58_to_psk(&refund)?; - let anchor = state.fetch_anchor()?; + let mut output_notes = Vec::with_capacity(4); + let mut outputs_values = Vec::with_capacity(4); - let hash_outputs: Vec = outputs.iter().map(|o| o.0).collect(); - let hash_crossover = crossover.map(|c| c.0); + for types::ExecuteOutput { + note_type, + receiver, + ref_id, + value, + } in outputs.into_iter() + { + let r#type = match note_type { + types::OutputType::Transparent => NoteType::Transparent, + types::OutputType::Obfuscated => NoteType::Obfuscated, + }; + + let r = JubJubScalar::random(rng); + let blinder = JubJubScalar::random(rng); + let nonce = BlsScalar::from(ref_id.unwrap_or_default()); + let receiver = utils::bs58_to_psk(&receiver)?; + let note = Note::deterministic( + r#type, &r, nonce, &receiver, value, blinder, + ); + + output_notes.push(note); + outputs_values.push(Output { + note, + value, + blinder, + }); + } - 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 outputs = outputs_values; + + let call = match call { + Some(types::ExecuteCall { + contract, + method, + payload, + }) => { + let decoded = bs58::decode(contract).into_vec().ok()?; + if decoded.len() != mem::size_of::() { + return None; + } + let mut contract = ContractId::uninitialized(); + contract.as_bytes_mut().copy_from_slice(&decoded); + Some(CallData { + contract, + method, + payload, + }) + } + None => None, + }; + let call_phoenix = call.as_ref().map(|c| { + (c.contract.to_bytes(), c.method.clone(), c.payload.clone()) + }); + + let fee = Fee::new(rng, gas_limit, gas_price, &refund); + + let crossover = crossover.map(|crossover| { + 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 +290,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/types.rs b/src/types.rs new file mode 100644 index 0000000..f58ac56 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,143 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. +// +// Copyright (c) DUSK NETWORK. All rights reserved. + +//! Arguments and responses to the module requests + +// THIS FILE IS AUTO GENERATED!! + +#![allow(missing_docs)] + +use alloc::string::String; +use alloc::vec::Vec; +use serde::{Deserialize, Serialize}; + +#[doc = " The arguments of the balance function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct BalanceArgs { + #[doc = " A rkyv serialized [Vec]; all notes should have their keys derived from "] + #[doc = " `seed`"] + pub notes: Vec, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " The response of the balance function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct BalanceResponse { + #[doc = " Maximum value per transaction"] + pub maximum: u64, + #[doc = " Total computed balance"] + pub value: u64, +} +#[doc = " The arguments of the execute function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteArgs { + #[doc = " A call to a contract method"] + #[serde(skip_serializing_if = "Option::is_none")] + pub call: Option, + #[doc = " The [phoenix_core::Crossover] value"] + #[serde(skip_serializing_if = "Option::is_none")] + pub crossover: Option, + #[doc = " The gas limit of the transaction"] + pub gas_limit: u64, + #[doc = " The gas price per unit for the transaction"] + pub gas_price: u64, + #[doc = " A rkyv serialized [Vec] to be used as inputs"] + pub inputs: Vec, + #[doc = " A rkyv serialized [Vec] to open the inputs to a Merkle root"] + pub openings: Vec, + #[doc = " The transfer output note"] + #[serde(skip_serializing_if = "Option::is_none")] + pub output: Option, + #[doc = " The refund addressin Base58 format"] + pub refund: String, + #[doc = " Seed used to derive the entropy for the notes"] + pub rng_seed: Vec, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " A call to a contract method"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteCall { + #[doc = " The id of the contract to call in Base58 format"] + pub contract: String, + #[doc = " The name of the method to be called"] + pub method: String, + #[doc = " The payload of the call"] + pub payload: Vec, +} +#[doc = " The output of a transfer"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteOutput { + #[doc = " The type of the note"] + pub note_type: OutputType, + #[doc = " The address of the receiver in Base58 format"] + pub receiver: String, + #[doc = " A reference id to be appended to the output"] + #[serde(skip_serializing_if = "Option::is_none")] + pub ref_id: Option, + #[doc = " The value of the output"] + pub value: u64, +} +#[doc = " The response of the execute function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ExecuteResponse { + #[doc = " A rkyv serialized [crate::tx::UnspentTransaction]"] + pub tx: Vec, + #[doc = " A rkyv serialized [Vec] containing the notes that weren't used"] + pub unspent: Vec, +} +#[doc = " The arguments of the filter_notes function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct FilterNotesArgs { + #[doc = " Boolean flags to be negative filtered"] + pub flags: Vec, + #[doc = " A rkyv serialized [Vec] to be filtered"] + pub notes: Vec, +} +#[doc = " The arguments of the merge_notes function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct MergeNotesArgs { + #[doc = " All serialized list of notes to be merged"] + pub notes: Vec>, +} +#[doc = " The arguments of the nullifiers function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct NullifiersArgs { + #[doc = " A rkyv serialized [Vec] to have nullifiers generated"] + pub notes: Vec, + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " A note type variant"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub enum OutputType { + Transparent, + Obfuscated, +} +#[doc = " The arguments of the public_spend_keys function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct PublicSpendKeysArgs { + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} +#[doc = " The response of the public_spend_keys function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct PublicSpendKeysResponse { + #[doc = " The Base58 public spend keys of the wallet."] + pub keys: Vec, +} +#[doc = " The arguments of the seed function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct SeedArgs { + #[doc = " An arbitrary sequence of bytes used to generate a secure seed"] + pub passphrase: Vec, +} +#[doc = " The arguments of the view_keys function"] +#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)] +pub struct ViewKeysArgs { + #[doc = " Seed used to derive the keys of the wallet"] + pub seed: Vec, +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..b6b4608 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,251 @@ +// 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, MAX_LEN, RNG_SEED}; + +use alloc::vec::Vec; +use core::mem; + +use dusk_bytes::DeserializableSlice; +use dusk_pki::PublicSpendKey; +use phoenix_core::Note; +use rand_chacha::ChaCha12Rng; +use rand_core::SeedableRng; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; + +/// Composes a `i64` from the provided arguments. This will be returned from the +/// WASM module functions. +pub const fn compose(success: bool, ptr: u32, len: u32) -> i64 { + let success = (!success) as u64; + let ptr = (ptr as u64) << 32; + let len = ((len as u64) << 48) >> 32; + (success | ptr | len) as i64 +} + +/// Decomposes a `i64` into its inner arguments, being: +/// +/// - status: a boolean indicating the success of the operation +/// - ptr: a pointer to the underlying data +/// - len: the length of the underlying data +pub const fn decompose(result: i64) -> (bool, u64, u64) { + let ptr = (result >> 32) as u64; + let len = ((result << 32) >> 48) as u64; + let success = ((result << 63) >> 63) == 0; + + (success, ptr, len) +} + +/// Takes a JSON string from the memory slice and deserializes it into the +/// provided type. +pub fn take_args(args: i32, len: i32) -> Option +where + T: for<'a> Deserialize<'a>, +{ + let args = args as *mut u8; + let len = len as usize; + let args = unsafe { Vec::from_raw_parts(args, len, len) }; + let args = alloc::string::String::from_utf8(args).ok()?; + serde_json::from_str(&args).ok() +} + +/// Sanitizes arbitrary bytes into well-formed seed. +pub fn sanitize_seed(bytes: Vec) -> Option<[u8; RNG_SEED]> { + (bytes.len() == RNG_SEED).then(|| { + let mut seed = [0u8; RNG_SEED]; + seed.copy_from_slice(&bytes); + seed + }) +} + +/// Fails the operation +pub const fn fail() -> i64 { + compose(false, 0, 0) +} + +/// Converts the provided response into an allocated pointer and returns the +/// composed success value. +pub fn into_ptr(response: T) -> i64 +where + T: Serialize, +{ + let response = serde_json::to_string(&response).unwrap_or_default(); + let ptr = response.as_ptr() as u32; + let len = response.len() as u32; + let result = compose(true, ptr, len); + + mem::forget(response); + + result +} + +/// Returns the provided bytes as a pointer +pub fn rkyv_into_ptr(value: T) -> i64 +where + T: rkyv::Serialize>, +{ + let bytes = match rkyv::to_bytes(&value) { + Ok(t) => t.into_vec(), + Err(_) => return fail(), + }; + + let ptr = bytes.as_ptr() as u32; + let len = bytes.len() as u32; + + mem::forget(bytes); + compose(true, ptr, len) +} + +/// Creates a secure RNG from a seed. +pub fn rng(seed: &[u8; RNG_SEED]) -> ChaCha12Rng { + let mut hash = Sha256::new(); + + 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) +} + +/// Sanitize a notes input into a consumable notes set +pub fn sanitize_notes(mut notes: Vec) -> Vec { + notes.sort_by_key(|n| n.hash()); + notes.dedup(); + notes +} + +/// Converts a Base58 string into a [PublicSpendKey]. +pub fn bs58_to_psk(psk: &str) -> Option { + // TODO this should be defined in dusk-pki + let bytes = bs58::decode(psk).into_vec().ok()?; + PublicSpendKey::from_reader(&mut &bytes[..]).ok() +} + +/// Perform a knapsack algorithm to define the notes to be used as input. +/// +/// Returns a tuple containing (unspent, inputs). `unspent` contains the notes +/// 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 compose_works() { + assert_eq!(decompose(compose(true, 0, 0)), (true, 0, 0)); + assert_eq!(decompose(compose(false, 0, 0)), (false, 0, 0)); + assert_eq!(decompose(compose(false, 1, 0)), (false, 1, 0)); + assert_eq!(decompose(compose(false, 0, 1)), (false, 0, 1)); + assert_eq!(decompose(compose(false, 4837, 383)), (false, 4837, 383)); +} + +#[test] +fn knapsack_works() { + use core::mem; + 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..cdce6b9 100644 --- a/tests/wallet.rs +++ b/tests/wallet.rs @@ -6,82 +6,444 @@ //! 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_pki::PublicSpendKey; +use dusk_wallet_core::{tx, types, utils, MAX_KEY, MAX_LEN, RNG_SEED}; +use rusk_abi::ContractId; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use wasmer::{imports, Instance, Module, Store, Value}; + +#[test] +fn seed_works() { + let mut wallet = Wallet::default(); + + let seed = wallet.call("seed", json!({ + "passphrase": b"Taking a new step, uttering a new word, is what people fear most.".to_vec() + })).take_memory(); + + assert_eq!(seed.len(), RNG_SEED); +} + +#[test] +fn balance_works() { + let seed = [0xfa; RNG_SEED]; + let values = [10, 250, 15, 39, 55]; + let mut wallet = Wallet::default(); + + let types::BalanceResponse { maximum, value } = wallet + .call( + "balance", + json!({ + "notes": node::notes(&seed, values), + "seed": seed.to_vec(), + }), + ) + .take_contents(); + + assert_eq!(value, values.into_iter().sum::()); + assert_eq!(maximum, 359); +} + +#[test] +fn execute_works() { + let seed = [0xfa; RNG_SEED]; + let rng_seed = [0xfb; RNG_SEED]; + let values = [10, 250, 15, 7500]; + + let mut wallet = Wallet::default(); + + let types::PublicSpendKeysResponse { keys } = wallet + .call( + "public_spend_keys", + json!({ + "seed": seed.to_vec(), + }), + ) + .take_contents(); + let psk = &keys[0]; + + let mut contract = ContractId::uninitialized(); + contract.as_bytes_mut().iter_mut().for_each(|b| *b = 0xfa); + let contract = bs58::encode(contract.as_bytes()).into_string(); + + let (inputs, openings) = node::notes_and_openings(&seed, values); + let args = json!({ + "call": { + "contract": contract, + "method": "commit", + "payload": b"We lost because we told ourselves we lost.".to_vec(), + }, + "crossover": 25, + "gas_limit": 100, + "gas_price": 2, + "inputs": inputs, + "openings": openings, + "output": { + "note_type": "Transparent", + "receiver": psk, + "ref_id": 15, + "value": 10, + }, + "refund": psk, + "rng_seed": rng_seed.to_vec(), + "seed": seed.to_vec() + }); + let types::ExecuteResponse { tx, unspent } = + wallet.call("execute", args).take_contents(); + + rkyv::from_bytes::(&tx).unwrap(); + rkyv::from_bytes::>(&unspent).unwrap(); +} #[test] -fn serde_stake() { - let stake = StakeInfo { - amount: Some((1000, 0)), - reward: 100, - counter: 1, - }; - - let stake_bytes = stake.to_bytes(); - let des_stake = - StakeInfo::from_bytes(&stake_bytes).expect("serde to go correctly"); - - assert_eq!(stake.amount, des_stake.amount); - assert_eq!(stake.reward, des_stake.reward); - assert_eq!(stake.counter, des_stake.counter); +fn merge_notes_works() { + let seed = [0xfa; RNG_SEED]; + + let notes1 = node::raw_notes(&seed, [10, 250, 15, 39, 55]); + let notes2 = vec![notes1[1].clone(), notes1[3].clone()]; + let notes3: Vec<_> = node::raw_notes(&seed, [10, 250, 15, 39, 55]) + .into_iter() + .chain([notes1[4].clone()]) + .collect(); + + let notes_unmerged: Vec<_> = notes1 + .iter() + .chain(notes2.iter()) + .chain(notes3.iter()) + .cloned() + .collect(); + + let mut notes_merged = notes_unmerged.clone(); + notes_merged.sort_by_key(|n| n.hash()); + notes_merged.dedup(); + + assert_ne!(notes_unmerged, notes_merged); + + let notes1 = rkyv::to_bytes::<_, MAX_LEN>(¬es1).unwrap().into_vec(); + let notes2 = rkyv::to_bytes::<_, MAX_LEN>(¬es2).unwrap().into_vec(); + let notes3 = rkyv::to_bytes::<_, MAX_LEN>(¬es3).unwrap().into_vec(); + let notes4 = vec![]; + let notes = vec![notes1, notes2, notes3, notes4]; + + let mut wallet = Wallet::default(); + + let notes = wallet + .call("merge_notes", json!({ "notes": notes })) + .take_memory(); + + let notes = rkyv::from_bytes::>(¬es).unwrap(); + + assert_eq!(notes, notes_merged); } #[test] -fn serde() { - let mut rng = rand::thread_rng(); +fn filter_notes_works() { + let seed = [0xfa; RNG_SEED]; + + let notes = node::raw_notes(&seed, [10, 250, 15, 39, 55]); + let flags = vec![true, true, false, true, false]; + let filtered = vec![notes[2].clone(), notes[4].clone()]; + let filtered = utils::sanitize_notes(filtered); + + let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es).unwrap().into_vec(); - let 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 notes = wallet + .call("filter_notes", json!({ "flags": flags, "notes": notes })) + .take_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"); + let notes = rkyv::from_bytes::>(¬es).unwrap(); + + assert_eq!(notes, filtered); } #[test] -fn canon() { - let mut rng = rand::thread_rng(); +fn public_spend_keys_works() { + let seed = [0xfa; RNG_SEED]; + + let mut wallet = Wallet::default(); - let wallet = mock_canon_wallet(&mut rng, &[2500, 2500, 5000]); + let types::PublicSpendKeysResponse { keys } = wallet + .call( + "public_spend_keys", + json!({ + "seed": seed.to_vec(), + }), + ) + .take_contents(); - let send_psk = wallet.public_spend_key(0).unwrap(); - let recv_psk = wallet.public_spend_key(1).unwrap(); + for key in &keys { + let key = bs58::decode(key).into_vec().unwrap(); + let mut key_array = [0u8; PublicSpendKey::SIZE]; + key_array.copy_from_slice(&key); + PublicSpendKey::from_bytes(&key_array).unwrap(); + } - 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"); + assert_eq!(keys.len(), MAX_KEY + 1); } #[test] -fn transfer() { - let mut rng = rand::thread_rng(); +fn view_keys_works() { + let seed = [0xfa; RNG_SEED]; - let wallet = mock_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 vk = wallet + .call( + "view_keys", + json!({ + "seed": seed.to_vec() + }), + ) + .take_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"); + rkyv::from_bytes::>(&vk).unwrap(); } #[test] -fn get_balance() { - let mut rng = rand::thread_rng(); +fn nullifiers_works() { + let seed = [0xfa; RNG_SEED]; + + let (notes, nullifiers): (Vec<_>, Vec<_>) = + node::raw_notes_and_nulifiers(&seed, [10, 250, 15, 39, 55]) + .into_iter() + .unzip(); + + let notes = rkyv::to_bytes::<_, MAX_LEN>(¬es).unwrap().into_vec(); + + let mut wallet = Wallet::default(); + + let response = wallet + .call( + "nullifiers", + json!({ + "seed": seed.to_vec(), + "notes": notes + }), + ) + .take_memory(); + + let response = + rkyv::from_bytes::>(&response).unwrap(); + + assert_eq!(nullifiers, response); +} + +/// A node interface. It will encapsulate all the phoenix core functionality. +mod node { + use core::mem; + + use dusk_jubjub::{BlsScalar, JubJubScalar}; + use dusk_wallet_core::{key, tx, utils, MAX_KEY, MAX_LEN, RNG_SEED}; + use phoenix_core::Note; + use rand::RngCore; + + pub fn raw_notes(seed: &[u8; RNG_SEED], values: Values) -> Vec + where + Values: IntoIterator, + { + let rng = &mut utils::rng(seed); + values + .into_iter() + .map(|value| { + let obfuscated = (rng.next_u32() & 1) == 1; + let idx = rng.next_u64() % MAX_KEY as u64; + let 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() + } + + pub fn notes(seed: &[u8; RNG_SEED], values: Values) -> Vec + where + Values: IntoIterator, + { + rkyv::to_bytes::<_, MAX_LEN>(&raw_notes(seed, values)) + .expect("failed to serialize notes") + .into_vec() + } + + pub fn notes_and_openings( + seed: &[u8; RNG_SEED], + values: Values, + ) -> (Vec, Vec) + where + Values: IntoIterator, + { + let values: Vec<_> = values.into_iter().collect(); + let len = values.len(); + let notes = notes(seed, values); + let openings: Vec<_> = (0..len) + .map(|_| unsafe { mem::zeroed::() }) + .collect(); + + let openings = rkyv::to_bytes::<_, MAX_LEN>(&openings) + .expect("failed to serialize openings") + .into_vec(); + + (notes, openings) + } + + pub fn raw_notes_and_nulifiers( + seed: &[u8; RNG_SEED], + values: Values, + ) -> Vec<(Note, BlsScalar)> + where + Values: IntoIterator, + { + let rng = &mut utils::rng(seed); + values + .into_iter() + .map(|value| { + let obfuscated = (rng.next_u32() & 1) == 1; + let idx = rng.next_u64() % MAX_KEY as u64; + let ssk = key::derive_ssk(seed, idx); + let psk = ssk.public_spend_key(); + + let note = if obfuscated { + let blinder = JubJubScalar::random(rng); + Note::obfuscated(rng, &psk, value, blinder) + } else { + Note::transparent(rng, &psk, value) + }; + + let nullifier = note.gen_nullifier(&ssk); + (note, nullifier) + }) + .collect() + } +} + +pub struct Wallet { + pub store: Store, + pub module: Module, + pub instance: Instance, +} + +pub struct CallResult<'a> { + pub status: bool, + pub val: u64, + pub aux: u64, + pub wallet: &'a mut Wallet, +} + +impl<'a> CallResult<'a> { + pub fn new(wallet: &'a mut Wallet, value: i64) -> Self { + let (status, val, aux) = utils::decompose(value); + Self { + status, + val, + aux, + wallet, + } + } + + pub fn take_memory(self) -> Vec { + assert!(self.status); + + let mut bytes = vec![0u8; self.aux as usize]; + + self.wallet + .instance + .exports + .get_memory("memory") + .unwrap() + .view(&self.wallet.store) + .read(self.val, &mut bytes) + .unwrap(); + + self.wallet + .instance + .exports + .get_function("free_mem") + .unwrap() + .call( + &mut self.wallet.store, + &[Value::I32(self.val as i32), Value::I32(self.aux as i32)], + ) + .unwrap(); + + bytes + } + + pub fn take_contents(self) -> T + where + T: for<'b> Deserialize<'b>, + { + assert!(self.status); + let bytes = self.take_memory(); + let json = String::from_utf8(bytes).unwrap(); + serde_json::from_str(&json).unwrap() + } + + pub fn take_val(self) -> u64 { + assert!(self.status); + self.val + } +} + +impl Wallet { + pub fn call(&mut self, f: &str, args: T) -> CallResult + where + T: Serialize, + { + let bytes = serde_json::to_string(&args).unwrap(); + let len = Value::I32(bytes.len() as i32); + let ptr = self + .instance + .exports + .get_function("malloc") + .unwrap() + .call(&mut self.store, &[len.clone()]) + .unwrap()[0] + .unwrap_i32(); + + self.instance + .exports + .get_memory("memory") + .unwrap() + .view(&self.store) + .write(ptr as u64, bytes.as_bytes()) + .unwrap(); + + let ptr = Value::I32(ptr); + let result = self + .instance + .exports + .get_function(f) + .unwrap() + .call(&mut self.store, &[ptr, len]) + .unwrap()[0] + .unwrap_i64(); + + CallResult::new(self, result) + } +} + +impl Default for Wallet { + fn default() -> Self { + const WALLET: &[u8] = include_bytes!("../assets/dusk_wallet_core.wasm"); + + let mut store = Store::default(); + let module = + Module::new(&store, WALLET).expect("failed to create wasm module"); - let wallet = mock_wallet(&mut rng, &[2500, 5000, 2500, 5000, 5000]); - let info = wallet.get_balance(0).expect("Valid balance call"); + let import_object = imports! {}; + let instance = Instance::new(&mut store, &module, &import_object) + .expect("failed to instanciate the wasm module"); - assert_eq!(info.value, 20000); - assert_eq!(info.spendable, 17500); + Self { + store, + module, + instance, + } + } }