use crate::tx::UnprovenTransaction;
use crate::{
BalanceInfo, ProverClient, StakeInfo, StateClient, Store, Transaction,
};
use alloc::vec::Vec;
use canonical::EncodeToVec;
use canonical::{Canon, CanonError};
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_poseidon::cipher::PoseidonCipher;
use dusk_poseidon::sponge;
use dusk_schnorr::Signature as SchnorrSignature;
use phoenix_core::{Crossover, Error as PhoenixError, Fee, Note, NoteType};
use rand_core::{CryptoRng, Error as RngError, RngCore};
use rusk_abi::ContractId;
const MAX_INPUT_NOTES: usize = 4;
#[derive(Debug)]
pub enum Error<S: Store, SC: StateClient, PC: ProverClient> {
Store(S::Error),
State(SC::Error),
Prover(PC::Error),
Canon(CanonError),
Rng(RngError),
Bytes(BytesError),
Phoenix(PhoenixError),
NotEnoughBalance,
NoteCombinationProblem,
AlreadyStaked {
key: PublicKey,
stake: StakeInfo,
},
NotStaked {
key: PublicKey,
stake: StakeInfo,
},
NoReward {
key: PublicKey,
stake: StakeInfo,
},
}
impl<S: Store, SC: StateClient, PC: ProverClient> Error<S, SC, PC> {
pub fn from_store_err(se: S::Error) -> Self {
Self::Store(se)
}
pub fn from_state_err(se: SC::Error) -> Self {
Self::State(se)
}
pub fn from_prover_err(pe: PC::Error) -> Self {
Self::Prover(pe)
}
}
impl<S: Store, SC: StateClient, PC: ProverClient> From<RngError>
for Error<S, SC, PC>
{
fn from(re: RngError) -> Self {
Self::Rng(re)
}
}
impl<S: Store, SC: StateClient, PC: ProverClient> From<BytesError>
for Error<S, SC, PC>
{
fn from(be: BytesError) -> Self {
Self::Bytes(be)
}
}
impl<S: Store, SC: StateClient, PC: ProverClient> From<PhoenixError>
for Error<S, SC, PC>
{
fn from(pe: PhoenixError) -> Self {
Self::Phoenix(pe)
}
}
impl<S: Store, SC: StateClient, PC: ProverClient> From<CanonError>
for Error<S, SC, PC>
{
fn from(ce: CanonError) -> Self {
Self::Canon(ce)
}
}
pub struct Wallet<S, SC, PC> {
store: S,
state: SC,
prover: PC,
}
impl<S, SC, PC> Wallet<S, SC, PC> {
pub const fn new(store: S, state: SC, prover: PC) -> Self {
Self {
store,
state,
prover,
}
}
pub const fn store(&self) -> &S {
&self.store
}
pub const fn state(&self) -> &SC {
&self.state
}
pub const fn prover(&self) -> &PC {
&self.prover
}
}
const TX_STAKE: u8 = 0x00;
const TX_UNSTAKE: u8 = 0x01;
const TX_WITHDRAW: u8 = 0x02;
const TX_ADD_ALLOWLIST: u8 = 0x03;
impl<S, SC, PC> Wallet<S, SC, PC>
where
S: Store,
SC: StateClient,
PC: ProverClient,
{
pub fn public_spend_key(
&self,
index: u64,
) -> Result<PublicSpendKey, Error<S, SC, PC>> {
self.store
.retrieve_ssk(index)
.map(|ssk| ssk.public_spend_key())
.map_err(Error::from_store_err)
}
pub fn public_key(
&self,
index: u64,
) -> Result<PublicKey, Error<S, SC, PC>> {
self.store
.retrieve_sk(index)
.map(|sk| From::from(&sk))
.map_err(Error::from_store_err)
}
fn unspent_notes(
&self,
ssk: &SecretSpendKey,
) -> Result<Vec<Note>, Error<S, SC, PC>> {
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)
}
#[allow(clippy::type_complexity)]
fn inputs_and_change_output<Rng: RngCore + CryptoRng>(
&self,
rng: &mut Rng,
sender: &SecretSpendKey,
refund: &PublicSpendKey,
value: u64,
) -> Result<
(
Vec<(Note, u64, JubJubScalar)>,
Vec<(Note, u64, JubJubScalar)>,
),
Error<S, SC, PC>,
> {
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::<u64>() - 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))
}
#[allow(clippy::too_many_arguments)]
pub fn execute<Rng, C>(
&self,
rng: &mut Rng,
contract_id: ContractId,
call_data: C,
sender_index: u64,
refund: &PublicSpendKey,
gas_limit: u64,
gas_price: u64,
) -> Result<Transaction, Error<S, SC, PC>>
where
Rng: RngCore + CryptoRng,
C: Canon,
{
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 = (contract_id, call_data.encode_to_vec());
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)
}
#[allow(clippy::too_many_arguments)]
pub fn transfer<Rng: RngCore + CryptoRng>(
&self,
rng: &mut Rng,
sender_index: u64,
refund: &PublicSpendKey,
receiver: &PublicSpendKey,
value: u64,
gas_limit: u64,
gas_price: u64,
ref_id: BlsScalar,
) -> Result<Transaction, Error<S, SC, PC>> {
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)
}
#[allow(clippy::too_many_arguments)]
pub fn stake<Rng: RngCore + CryptoRng>(
&self,
rng: &mut Rng,
sender_index: u64,
staker_index: u64,
refund: &PublicSpendKey,
value: u64,
gas_limit: u64,
gas_price: u64,
) -> Result<Transaction, Error<S, SC, PC>> {
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 stct_signature =
sign_stct(rng, &sender, &fee, &crossover, value, &address);
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 call_data =
(TX_STAKE, pk, signature, value, spend_proof).encode_to_vec();
let call = (contract_id, 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)
}
pub fn unstake<Rng: RngCore + CryptoRng>(
&self,
rng: &mut Rng,
sender_index: u64,
staker_index: u64,
refund: &PublicSpendKey,
gas_limit: u64,
gas_price: u64,
) -> Result<Transaction, Error<S, SC, PC>> {
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);
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 call_data =
(TX_UNSTAKE, pk, signature, unstake_note, unstake_proof)
.encode_to_vec();
let contract_id = rusk_abi::stake_contract();
let call = (contract_id, 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)
}
pub fn withdraw<Rng: RngCore + CryptoRng>(
&self,
rng: &mut Rng,
sender_index: u64,
staker_index: u64,
refund: &PublicSpendKey,
gas_limit: u64,
gas_price: u64,
) -> Result<Transaction, Error<S, SC, PC>> {
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);
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 call_data =
(TX_WITHDRAW, pk, signature, address, nonce).encode_to_vec();
let contract_id = rusk_abi::stake_contract();
let call = (contract_id, 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(clippy::too_many_arguments)]
pub fn allow<Rng: RngCore + CryptoRng>(
&self,
rng: &mut Rng,
sender_index: u64,
owner_index: u64,
refund: &PublicSpendKey,
staker: &PublicKey,
gas_limit: u64,
gas_price: u64,
) -> Result<Transaction, Error<S, SC, PC>> {
let sender = self
.store
.retrieve_ssk(sender_index)
.map_err(Error::from_store_err)?;
let sk = self
.store
.retrieve_sk(owner_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 signature = allow_sign(&sk, &pk, stake.counter, staker);
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 call_data =
(TX_ADD_ALLOWLIST, *staker, pk, signature).encode_to_vec();
let contract_id = rusk_abi::stake_contract();
let call = (contract_id, 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)
}
pub fn get_balance(
&self,
ssk_index: u64,
) -> Result<BalanceInfo, Error<S, SC, PC>> {
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::<u64>();
Ok(BalanceInfo { value, spendable })
}
pub fn get_stake(
&self,
sk_index: u64,
) -> Result<StakeInfo, Error<S, SC, PC>> {
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)
}
}
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::<u64>()
>= value
})
.map(|indices| {
indices
.into_iter()
.map(|index| notes_and_values[index])
.collect()
})
.unwrap_or_default()
}
fn pick_lexicographic<F: Fn(&[usize; MAX_INPUT_NOTES]) -> 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
}
const STCT_MESSAGE_SIZE: usize = 5 + PoseidonCipher::cipher_size();
fn sign_stct<Rng: RngCore + CryptoRng>(
rng: &mut Rng,
ssk: &SecretSpendKey,
fee: &Fee,
crossover: &Crossover,
value: u64,
address: &BlsScalar,
) -> SchnorrSignature {
let sk_r = *ssk.sk_r(fee.stealth_address()).as_ref();
let secret = SchnorrKey::from(sk_r);
let message = {
let mut message = [BlsScalar::zero(); STCT_MESSAGE_SIZE];
let mut m = message.iter_mut();
crossover
.value_commitment()
.to_hash_inputs()
.iter()
.zip(m.by_ref())
.for_each(|(c, m)| *m = *c);
if let Some(m) = m.next() {
*m = *crossover.nonce();
}
crossover
.encrypted_data()
.cipher()
.iter()
.zip(m.by_ref())
.for_each(|(c, m)| *m = *c);
if let Some(m) = m.next() {
*m = value.into();
}
if let Some(m) = m.next() {
*m = *address;
}
sponge::hash(&message)
};
SchnorrSignature::new(&secret, rng, message)
}
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)
}
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)
}
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)
}
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)
}
fn generate_obfuscated_note<Rng: RngCore + CryptoRng>(
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::<u64>(), 20);
}
}