Skip to content

Commit

Permalink
wallet-core: Add input module
Browse files Browse the repository at this point in the history
- Add algorithm for picking notes
- Re-export some commonly used types
  • Loading branch information
Daksh14 committed Sep 5, 2024
1 parent a29c2af commit 2b4e884
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 54 deletions.
2 changes: 1 addition & 1 deletion wallet-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ sha2 = { version = "0.10", default-features = false }
rand = { version = "0.8", default-features = false }
ff = { version = "0.13", default-features = false }
poseidon-merkle = { version = "0.7", features = ["rkyv-impl"] }
execution-core = { version = "0.1", path = "../execution-core/" }
execution-core = { version = "0.1", path = "../execution-core/", default-features = false }

[target.'cfg(target_family = "wasm")'.dependencies]
dlmalloc = { version = "0.2", features = ["global"] }
Expand Down
3 changes: 1 addition & 2 deletions wallet-core/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
//
// Copyright (c) DUSK NETWORK. All rights reserved.

use crate::keys::{derive_bls_pk, derive_phoenix_pk};
use crate::RNG_SEED;
use crate::keys::{derive_bls_pk, derive_phoenix_pk, RNG_SEED};
use core::ptr;
use dusk_bytes::Serializable;
use execution_core::{
Expand Down
128 changes: 128 additions & 0 deletions wallet-core/src/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// 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.

//! Helper functions for working with notes.

use alloc::vec::Vec;

use super::{alloc, Note};

use execution_core::BlsScalar;

/// The maximum amount of input notes that can be spend in one
/// phoenix-transaction
pub const MAX_INPUT_NOTES: usize = 4;

/// Pick the notes to be used in a transaction from a vector of notes.
///
/// The resulting array is only 4 notes long, the argument of this function can
/// be arbitary amount of notes.
///
/// # Errors
///
/// If the target sum is greater than the sum of the notes then an error is
/// returned. If the notes vector is empty then an error is returned.
///
/// See `InputNotesError` type for possible errors
/// this function can yield.
#[must_use]
pub fn try_input_notes(
nodes: Vec<(Note, u64, BlsScalar)>,
target_sum: u64,
) -> Vec<(Note, BlsScalar)> {
if nodes.is_empty() {
return Vec::new();
}

let mut i = 0;
let mut sum = 0;
while sum < target_sum && i < nodes.len() {
sum = sum.saturating_add(nodes[i].1);
i += 1;
}

if sum < target_sum {
return Vec::new();
}

pick_notes(target_sum, nodes)
}

/// 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, BlsScalar)>,
) -> Vec<(Note, BlsScalar)> {
let mut notes_and_values = notes_and_values;
let len = notes_and_values.len();

if len <= MAX_INPUT_NOTES {
return notes_and_values
.into_iter()
.map(|(note, _, b)| (note, b))
.collect();
}

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::<u64>()
>= value
})
.map(|index| notes_and_values[index].clone())
.map(|(n, _, b)| (n, b))
.to_vec()
}

fn pick_lexicographic<F: Fn(&[usize; MAX_INPUT_NOTES]) -> bool>(
max_len: usize,
is_valid: F,
) -> [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 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;
}
}

[0; MAX_INPUT_NOTES]
}
6 changes: 5 additions & 1 deletion wallet-core/src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ use execution_core::{
},
};

use crate::RNG_SEED;
/// The seed bytes buffer which is used at multiple places
pub type Seed = [u8; RNG_SEED];

/// Length of the seed of the generated rng.
pub const RNG_SEED: usize = 64;

/// Generates a [`BlsSecretKey`] from a seed and index.
///
Expand Down
21 changes: 11 additions & 10 deletions wallet-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;

extern crate alloc;

pub mod keys;
pub mod transaction;

#[cfg(target_family = "wasm")]
mod ffi;

/// Length of the seed of the generated rng.
pub const RNG_SEED: usize = 64;
pub mod input;
pub mod keys;
pub mod transaction;

// The maximum amount of input notes that can be spend in one
// phoenix-transaction
const MAX_INPUT_NOTES: usize = 4;
pub mod prelude {
//! Re-export of the most commonly used types and traits.
pub use crate::keys;
pub use crate::{input::MAX_INPUT_NOTES, keys::RNG_SEED};
}

use alloc::collections::btree_map::BTreeMap;
use alloc::vec::Vec;
Expand Down Expand Up @@ -77,8 +77,9 @@ where

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::<u64>();
let spendable = values.iter().take(input::MAX_INPUT_NOTES).sum();
let value =
spendable + values.iter().skip(input::MAX_INPUT_NOTES).sum::<u64>();

BalanceInfo { value, spendable }
}
Expand Down
126 changes: 86 additions & 40 deletions wallet-core/tests/notes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,49 @@
// Copyright (c) DUSK NETWORK. All rights reserved.

use ff::Field;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use rand::{rngs::StdRng, CryptoRng, RngCore, SeedableRng};

use execution_core::{
transfer::phoenix::{
Note, NoteLeaf, PublicKey as PhoenixPublicKey,
SecretKey as PhoenixSecretKey,
},
JubJubScalar,
BlsScalar, JubJubScalar,
};

use wallet_core::{
keys::derive_multiple_phoenix_sk, map_owned, phoenix_balance, BalanceInfo,
input::try_input_notes, keys::derive_multiple_phoenix_sk, map_owned,
phoenix_balance, BalanceInfo,
};

/// Generate a note, useful for testing purposes
pub fn gen_note<T: RngCore + CryptoRng>(
rng: &mut T,
obfuscated_note: bool,
owner_pk: &PhoenixPublicKey,
value: u64,
) -> Note {
let sender_pk = PhoenixPublicKey::from(&PhoenixSecretKey::random(rng));

let value_blinder = JubJubScalar::random(&mut *rng);
let sender_blinder = [
JubJubScalar::random(&mut *rng),
JubJubScalar::random(&mut *rng),
];
if obfuscated_note {
Note::obfuscated(
rng,
&sender_pk,
owner_pk,
value,
value_blinder,
sender_blinder,
)
} else {
Note::transparent(rng, &sender_pk, owner_pk, value, sender_blinder)
}
}

#[test]
fn test_map_owned() {
// Assuming this set of notes where the number used as suffix is the
Expand Down Expand Up @@ -121,41 +149,59 @@ fn test_balance() {
);
}

fn gen_note(
rng: &mut StdRng,
obfuscated_note: bool,
owner_pk: &PhoenixPublicKey,
value: u64,
) -> NoteLeaf {
let sender_pk = PhoenixPublicKey::from(&PhoenixSecretKey::random(rng));
#[test]
fn knapsack_works() {
use rand::SeedableRng;

let value_blinder = JubJubScalar::random(&mut *rng);
let sender_blinder = [
JubJubScalar::random(&mut *rng),
JubJubScalar::random(&mut *rng),
];
if obfuscated_note {
NoteLeaf {
note: Note::obfuscated(
rng,
&sender_pk,
&owner_pk,
value,
value_blinder,
sender_blinder,
),
block_height: rng.gen(),
}
} else {
NoteLeaf {
note: Note::transparent(
rng,
&sender_pk,
&owner_pk,
value,
sender_blinder,
),
block_height: rng.gen(),
}
}
let mut rng = rand_chacha::ChaCha12Rng::seed_from_u64(0xbeef);

// sanity check
assert_eq!(try_input_notes(vec![], 70), Vec::new(),);

let sk = PhoenixSecretKey::random(&mut rng);
let pk = PhoenixPublicKey::from(&sk);

// basic check
let note = gen_note(&mut rng, true, &pk, 100);
let n = note.gen_nullifier(&sk);
let available = vec![(note, 100, n)];
let inputs_notes: Vec<(Note, BlsScalar)> = available
.clone()
.into_iter()
.map(|(a, _, b)| (a, b))
.collect();
let input = try_input_notes(available, 70);
assert_eq!(input, inputs_notes);

// out of balance basic check
let note = gen_note(&mut rng, true, &pk, 100);
let available = vec![(note, 100, n)];
assert_eq!(try_input_notes(available, 101), Vec::new());

// multiple inputs check
// note: this test is checking a naive, simple order-based output

let note1 = gen_note(&mut rng, true, &pk, 100);
let note2 = gen_note(&mut rng, true, &pk, 500);
let note3 = gen_note(&mut rng, true, &pk, 300);

let available = vec![(note1, 100, n), (note2, 500, n), (note3, 300, n)];

let result: Vec<(Note, BlsScalar)> = available
.clone()
.into_iter()
.map(|(a, _, b)| (a, b))
.collect();

assert_eq!(try_input_notes(available.clone(), 600), result);

let note1 = gen_note(&mut rng, true, &pk, 100);
let note2 = gen_note(&mut rng, true, &pk, 500);
let note3 = gen_note(&mut rng, true, &pk, 300);

let n = note1.gen_nullifier(&sk);

let available = vec![(note1, 100, n), (note2, 500, n), (note3, 300, n)];

assert_eq!(try_input_notes(available, 901), Vec::new());
}

0 comments on commit 2b4e884

Please sign in to comment.