From 607e2063083e469998a4b5b452404187f1255967 Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 16 Sep 2024 18:21:31 +0200 Subject: [PATCH 1/3] execution-core: Add unproven phoenix transaction support --- execution-core/src/transfer/phoenix.rs | 483 +++++++++++++++---------- 1 file changed, 287 insertions(+), 196 deletions(-) diff --git a/execution-core/src/transfer/phoenix.rs b/execution-core/src/transfer/phoenix.rs index d618d3ebe8..f2e3825e9c 100644 --- a/execution-core/src/transfer/phoenix.rs +++ b/execution-core/src/transfer/phoenix.rs @@ -108,7 +108,6 @@ impl Transaction { /// - the `inputs` vector contains duplicate `Note`s /// - the `prover` is implemented incorrectly /// - the memo, if given, is too large - #[allow(clippy::too_many_lines)] #[allow(clippy::too_many_arguments)] #[allow(clippy::similar_names)] pub fn new( @@ -127,199 +126,41 @@ impl Transaction { data: Option>, prover: &P, ) -> Result { - let data = data.map(Into::into); - - if let Some(TransactionData::Memo(memo)) = data.as_ref() { - if memo.len() > MAX_MEMO_SIZE { - return Err(Error::MemoTooLarge(memo.len())); - } - } - - let sender_pk = PublicKey::from(sender_sk); - let sender_vk = ViewKey::from(sender_sk); - - // get input note values, value-blinders and nullifiers - let input_len = inputs.len(); - let mut input_values = Vec::with_capacity(input_len); - let mut input_value_blinders = Vec::with_capacity(input_len); - let mut input_nullifiers = Vec::with_capacity(input_len); - for (note, _opening) in &inputs { - let note_nullifier = note.gen_nullifier(sender_sk); - for nullifier in &input_nullifiers { - if note_nullifier == *nullifier { - return Err(Error::Replay); - } - } - input_nullifiers.push(note_nullifier); - input_values.push(note.value(Some(&sender_vk))?); - input_value_blinders.push(note.value_blinder(Some(&sender_vk))?); - } - let input_value: u64 = input_values.iter().sum(); - - // --- Create the transaction payload - - // Set the fee. - let fee = Fee::new(rng, change_pk, gas_limit, gas_price); - let max_fee = fee.max_fee(); - - if input_value < transfer_value + max_fee + deposit { - return Err(Error::InsufficientBalance); - } - - // Generate output notes: - let transfer_value_blinder = if obfuscated_transaction { - JubJubScalar::random(&mut *rng) - } else { - JubJubScalar::zero() - }; - let transfer_sender_blinder = [ - JubJubScalar::random(&mut *rng), - JubJubScalar::random(&mut *rng), - ]; - let change_sender_blinder = [ - JubJubScalar::random(&mut *rng), - JubJubScalar::random(&mut *rng), - ]; - let transfer_note = if obfuscated_transaction { - Note::obfuscated( - rng, - &sender_pk, - receiver_pk, - transfer_value, - transfer_value_blinder, - transfer_sender_blinder, - ) - } else { - Note::transparent( - rng, - &sender_pk, - receiver_pk, - transfer_value, - transfer_sender_blinder, - ) - }; - // The change note should have the value of the input note, minus what - // is maximally spent. - let change_value = input_value - transfer_value - max_fee - deposit; - let change_value_blinder = JubJubScalar::random(&mut *rng); - let change_note = Note::obfuscated( + let unproven = UnprovenTransaction::new( rng, - &sender_pk, + sender_sk, change_pk, - change_value, - change_value_blinder, - change_sender_blinder, - ); - let outputs = [transfer_note.clone(), change_note.clone()]; - - // Now we can set the tx-skeleton, payload and get the payload-hash - let tx_skeleton = TxSkeleton { + receiver_pk, + inputs, root, - // we also need the nullifiers for the tx-circuit, hence the clone - nullifiers: input_nullifiers.clone(), - outputs, - max_fee, + transfer_value, + obfuscated_transaction, deposit, - }; - let payload = Payload { + gas_limit, + gas_price, chain_id, - tx_skeleton, - fee, data, - }; - let payload_hash = payload.hash(); - - // --- Create the transaction proof - - // Create a vector with all the information for the input-notes - let mut input_notes_info = Vec::with_capacity(input_len); - inputs - .into_iter() - .zip(input_nullifiers) - .zip(input_values) - .zip(input_value_blinders) - .for_each( - |( - (((note, merkle_opening), nullifier), value), - value_blinder, - )| { - let note_sk = sender_sk.gen_note_sk(note.stealth_address()); - let note_pk_p = JubJubAffine::from( - crate::GENERATOR_NUMS_EXTENDED * note_sk.as_ref(), - ); - let signature = note_sk.sign_double(rng, payload_hash); - input_notes_info.push(InputNoteInfo { - merkle_opening, - note, - note_pk_p, - value, - value_blinder, - nullifier, - signature, - }); - }, - ); - - // Create the information for the output-notes - let transfer_value_commitment = - value_commitment(transfer_value, transfer_value_blinder); - let transfer_note_sender_enc = match transfer_note.sender() { - Sender::Encryption(enc) => enc, - Sender::ContractInfo(_) => unreachable!("The sender is encrypted"), - }; - let change_value_commitment = - value_commitment(change_value, change_value_blinder); - let change_note_sender_enc = match change_note.sender() { - Sender::Encryption(enc) => enc, - Sender::ContractInfo(_) => unreachable!("The sender is encrypted"), - }; - let output_notes_info = [ - OutputNoteInfo { - value: transfer_value, - value_commitment: transfer_value_commitment, - value_blinder: transfer_value_blinder, - note_pk: JubJubAffine::from( - transfer_note.stealth_address().note_pk().as_ref(), - ), - sender_enc: *transfer_note_sender_enc, - sender_blinder: transfer_sender_blinder, - }, - OutputNoteInfo { - value: change_value, - value_commitment: change_value_commitment, - value_blinder: change_value_blinder, - note_pk: JubJubAffine::from( - change_note.stealth_address().note_pk().as_ref(), - ), - sender_enc: *change_note_sender_enc, - sender_blinder: change_sender_blinder, - }, - ]; - - // Sign the payload hash using both 'a' and 'b' of the sender_sk - let schnorr_sk_a = SchnorrSecretKey::from(sender_sk.a()); - let sig_a = schnorr_sk_a.sign(rng, payload_hash); - let schnorr_sk_b = SchnorrSecretKey::from(sender_sk.b()); - let sig_b = schnorr_sk_b.sign(rng, payload_hash); + )?; Ok(Self { - payload, - proof: prover.prove( - &TxCircuitVec { - input_notes_info, - output_notes_info, - payload_hash, - root, - deposit, - max_fee, - sender_pk, - signatures: (sig_a, sig_b), - } - .to_var_bytes(), - )?, + payload: unproven.payload, + proof: prover.prove(&unproven.circuit)?, }) } + /// Generates a [`Transaction`] from an [`UnprovenTransaction`] and the + /// proof as bytes. + #[must_use] + pub fn from_unproven( + unproven: UnprovenTransaction, + proof: Vec, + ) -> Self { + Self { + payload: unproven.payload, + proof, + } + } + /// Creates a new phoenix transaction given the [`Payload`] and proof. Note /// that this function doesn't guarantee that the proof matches the /// payload, if possible use [`Self::new`] instead. @@ -328,19 +169,6 @@ impl Transaction { Self { payload, proof } } - /// Replaces the inner `proof` bytes for a given `proof`. - /// - /// This can be used to delegate the proof generation after a - /// [`Transaction`] is created. - /// In order to do that, the transaction would be created using the - /// serialized circuit-bytes for the proof-field. Those bytes can be - /// sent to a 3rd-party prover-service that generates the proof-bytes - /// and sends them back. The proof-bytes will then replace the - /// circuit-bytes in the transaction using this function. - pub fn set_proof(&mut self, proof: Vec) { - self.proof = proof; - } - /// The proof of the transaction. #[must_use] pub fn proof(&self) -> &[u8] { @@ -615,6 +443,269 @@ impl Transaction { } } +/// Unproven Phoenix transaction to be used to decouple transaction creation +/// from proof generation. +/// +/// To generate a [`Transaction`] use [`Transaction::from_unproven`] with the +/// proof-bytes generated by passing the `circuit` bytes to an external prover. +#[derive(Debug, Clone, Archive, Serialize, Deserialize)] +#[archive_attr(derive(CheckBytes))] +pub struct UnprovenTransaction { + payload: Payload, + /// The transaction-circuit as bytes. + pub circuit: Vec, +} + +impl PartialEq for UnprovenTransaction { + fn eq(&self, other: &Self) -> bool { + self.hash() == other.hash() + } +} + +impl Eq for UnprovenTransaction {} + +impl UnprovenTransaction { + /// Create a new unproven phoenix transaction given the sender secret-key, + /// receiver public-key, the input note positions in the transaction + /// tree and the new output-notes. + /// In contrast to a proven [`Transaction`], an unproven transaction doesn't + /// carry a `proof` but the transaction-circuit as bytes. These + /// circuit-bytes can be passed to a prover to generate a proof. + /// + /// # Errors + /// The creation of a transaction is not possible and will error if: + /// - one of the input-notes cannot be decrypted using the `sender_sk` + /// - the transaction input doesn't cover the transaction costs + /// - the `inputs` vector is either empty or larger than 4 elements + /// - the `inputs` vector contains duplicate `Note`s + /// - the memo, if given, is too large + #[allow(clippy::too_many_lines)] + #[allow(clippy::too_many_arguments)] + #[allow(clippy::similar_names)] + pub fn new( + rng: &mut R, + sender_sk: &SecretKey, + change_pk: &PublicKey, + receiver_pk: &PublicKey, + inputs: Vec<(Note, NoteOpening)>, + root: BlsScalar, + transfer_value: u64, + obfuscated_transaction: bool, + deposit: u64, + gas_limit: u64, + gas_price: u64, + chain_id: u8, + data: Option>, + ) -> Result { + let data = data.map(Into::into); + + if let Some(TransactionData::Memo(memo)) = data.as_ref() { + if memo.len() > MAX_MEMO_SIZE { + return Err(Error::MemoTooLarge(memo.len())); + } + } + + let sender_pk = PublicKey::from(sender_sk); + let sender_vk = ViewKey::from(sender_sk); + + // get input note values, value-blinders and nullifiers + let input_len = inputs.len(); + let mut input_values = Vec::with_capacity(input_len); + let mut input_value_blinders = Vec::with_capacity(input_len); + let mut input_nullifiers = Vec::with_capacity(input_len); + for (note, _opening) in &inputs { + let note_nullifier = note.gen_nullifier(sender_sk); + for nullifier in &input_nullifiers { + if note_nullifier == *nullifier { + return Err(Error::Replay); + } + } + input_nullifiers.push(note_nullifier); + input_values.push(note.value(Some(&sender_vk))?); + input_value_blinders.push(note.value_blinder(Some(&sender_vk))?); + } + let input_value: u64 = input_values.iter().sum(); + + // --- Create the transaction payload + + // Set the fee. + let fee = Fee::new(rng, change_pk, gas_limit, gas_price); + let max_fee = fee.max_fee(); + + if input_value < transfer_value + max_fee + deposit { + return Err(Error::InsufficientBalance); + } + + // Generate output notes: + let transfer_value_blinder = if obfuscated_transaction { + JubJubScalar::random(&mut *rng) + } else { + JubJubScalar::zero() + }; + let transfer_sender_blinder = [ + JubJubScalar::random(&mut *rng), + JubJubScalar::random(&mut *rng), + ]; + let change_sender_blinder = [ + JubJubScalar::random(&mut *rng), + JubJubScalar::random(&mut *rng), + ]; + let transfer_note = if obfuscated_transaction { + Note::obfuscated( + rng, + &sender_pk, + receiver_pk, + transfer_value, + transfer_value_blinder, + transfer_sender_blinder, + ) + } else { + Note::transparent( + rng, + &sender_pk, + receiver_pk, + transfer_value, + transfer_sender_blinder, + ) + }; + // The change note should have the value of the input note, minus what + // is maximally spent. + let change_value = input_value - transfer_value - max_fee - deposit; + let change_value_blinder = JubJubScalar::random(&mut *rng); + let change_note = Note::obfuscated( + rng, + &sender_pk, + change_pk, + change_value, + change_value_blinder, + change_sender_blinder, + ); + let outputs = [transfer_note.clone(), change_note.clone()]; + + // Now we can set the tx-skeleton, payload and get the payload-hash + let tx_skeleton = TxSkeleton { + root, + // we also need the nullifiers for the tx-circuit, hence the clone + nullifiers: input_nullifiers.clone(), + outputs, + max_fee, + deposit, + }; + let payload = Payload { + chain_id, + tx_skeleton, + fee, + data, + }; + let payload_hash = payload.hash(); + + // --- Create the transaction proof + + // Create a vector with all the information for the input-notes + let mut input_notes_info = Vec::with_capacity(input_len); + inputs + .into_iter() + .zip(input_nullifiers) + .zip(input_values) + .zip(input_value_blinders) + .for_each( + |( + (((note, merkle_opening), nullifier), value), + value_blinder, + )| { + let note_sk = sender_sk.gen_note_sk(note.stealth_address()); + let note_pk_p = JubJubAffine::from( + crate::GENERATOR_NUMS_EXTENDED * note_sk.as_ref(), + ); + let signature = note_sk.sign_double(rng, payload_hash); + input_notes_info.push(InputNoteInfo { + merkle_opening, + note, + note_pk_p, + value, + value_blinder, + nullifier, + signature, + }); + }, + ); + + // Create the information for the output-notes + let transfer_value_commitment = + value_commitment(transfer_value, transfer_value_blinder); + let transfer_note_sender_enc = match transfer_note.sender() { + Sender::Encryption(enc) => enc, + Sender::ContractInfo(_) => unreachable!("The sender is encrypted"), + }; + let change_value_commitment = + value_commitment(change_value, change_value_blinder); + let change_note_sender_enc = match change_note.sender() { + Sender::Encryption(enc) => enc, + Sender::ContractInfo(_) => unreachable!("The sender is encrypted"), + }; + let output_notes_info = [ + OutputNoteInfo { + value: transfer_value, + value_commitment: transfer_value_commitment, + value_blinder: transfer_value_blinder, + note_pk: JubJubAffine::from( + transfer_note.stealth_address().note_pk().as_ref(), + ), + sender_enc: *transfer_note_sender_enc, + sender_blinder: transfer_sender_blinder, + }, + OutputNoteInfo { + value: change_value, + value_commitment: change_value_commitment, + value_blinder: change_value_blinder, + note_pk: JubJubAffine::from( + change_note.stealth_address().note_pk().as_ref(), + ), + sender_enc: *change_note_sender_enc, + sender_blinder: change_sender_blinder, + }, + ]; + + // Sign the payload hash using both 'a' and 'b' of the sender_sk + let schnorr_sk_a = SchnorrSecretKey::from(sender_sk.a()); + let sig_a = schnorr_sk_a.sign(rng, payload_hash); + let schnorr_sk_b = SchnorrSecretKey::from(sender_sk.b()); + let sig_b = schnorr_sk_b.sign(rng, payload_hash); + + Ok(Self { + payload, + circuit: TxCircuitVec { + input_notes_info, + output_notes_info, + payload_hash, + root, + deposit, + max_fee, + sender_pk, + signatures: (sig_a, sig_b), + } + .to_var_bytes(), + }) + } + + /// Return input bytes to hash the Transaction. + /// + /// Note: The result of this function is *only* meant to be used as an input + /// for hashing and *cannot* be used to deserialize the `Transaction` again. + #[must_use] + pub fn to_hash_input_bytes(&self) -> Vec { + let mut bytes = self.payload.to_hash_input_bytes(); + bytes.extend(&self.circuit); + bytes + } + + /// Create the `Transaction`-hash. + #[must_use] + pub fn hash(&self) -> BlsScalar { + BlsScalar::hash_to_scalar(&self.to_hash_input_bytes()) + } +} + /// The transaction payload #[derive(Debug, Clone, Archive, Serialize, Deserialize)] #[archive_attr(derive(CheckBytes))] From 1a8f461df2368cfef126511d7c0796d0fdf4a563 Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 16 Sep 2024 18:22:01 +0200 Subject: [PATCH 2/3] wallet-core: Add support for unproven phoenix transaction --- wallet-core/src/transaction.rs | 228 +++----------- wallet-core/src/transaction/unproven.rs | 392 ++++++++++++++++++++++++ 2 files changed, 441 insertions(+), 179 deletions(-) create mode 100644 wallet-core/src/transaction/unproven.rs diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index 91afa02cc2..86ab938bbb 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/transaction.rs @@ -6,6 +6,8 @@ //! Implementations of basic wallet functionalities to create transactions. +pub mod unproven; + use alloc::vec::Vec; use dusk_bytes::Serializable; @@ -31,16 +33,8 @@ use execution_core::{ BlsScalar, ContractId, Error, JubJubScalar, }; -/// An unproven-transaction is nearly identical to a [`PhoenixTransaction`] with -/// the only difference being that it carries a serialized [`TxCircuitVec`] -/// instead of the proof bytes. -/// This way it is possible to delegate the proof generation of the -/// [`TxCircuitVec`] after the unproven transaction was created while at the -/// same time ensuring non-malleability of the transaction, as the transaction's -/// payload-hash is part of the public inputs of the circuit. -/// Once the proof is generated from the [`TxCircuitVec`] bytes, it can -/// replace the serialized circuit in the transaction by calling -/// [`Transaction::replace_proof`]. +/// Create a generic Phoenix [`Transaction`] with a proof being generated right +/// away by the given `prover`. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -151,33 +145,22 @@ pub fn phoenix_stake( current_nonce: u64, prover: &P, ) -> Result { - let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); - let change_pk = receiver_pk; - - let transfer_value = 0; - let obfuscated_transaction = false; - let deposit = stake_value; - - let stake = Stake::new(stake_sk, stake_value, current_nonce, chain_id); - - let contract_call = ContractCall::new(STAKE_CONTRACT, "stake", &stake)?; - - phoenix::( + let utx = unproven::phoenix_stake( rng, phoenix_sender_sk, - &change_pk, - &receiver_pk, + stake_sk, inputs, root, - transfer_value, - obfuscated_transaction, - deposit, gas_limit, gas_price, chain_id, - Some(contract_call), - prover, - ) + stake_value, + current_nonce, + )?; + + let proof = prover.prove(&utx.circuit)?; + + Ok(PhoenixTransaction::from_unproven(utx, proof).into()) } /// Create a [`Transaction`] to stake from a Moonlight account. @@ -222,8 +205,7 @@ pub fn moonlight_stake( ) } -/// Create an unproven [`Transaction`] to withdraw stake rewards into a -/// phoenix-note. +/// Create a [`Transaction`] to withdraw stake rewards into a phoenix-note. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -245,49 +227,21 @@ pub fn phoenix_stake_reward( chain_id: u8, prover: &P, ) -> Result { - let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); - let change_pk = receiver_pk; - - let transfer_value = 0; - let obfuscated_transaction = false; - let deposit = 0; - - // split the input notes and openings from the nullifiers - let mut nullifiers = Vec::with_capacity(inputs.len()); - let inputs = inputs - .into_iter() - .map(|(note, opening, nullifier)| { - nullifiers.push(nullifier); - (note, opening) - }) - .collect(); - - let gas_payment_token = WithdrawReplayToken::Phoenix(nullifiers); - - let contract_call = stake_reward_to_phoenix( + let utx = unproven::phoenix_stake_reward( rng, phoenix_sender_sk, stake_sk, - gas_payment_token, - reward_amount, - )?; - - phoenix::( - rng, - phoenix_sender_sk, - &change_pk, - &receiver_pk, inputs, root, - transfer_value, - obfuscated_transaction, - deposit, + reward_amount, gas_limit, gas_price, chain_id, - Some(contract_call), - prover, - ) + )?; + + let proof = prover.prove(&utx.circuit)?; + + Ok(PhoenixTransaction::from_unproven(utx, proof).into()) } /// Create a [`Transaction`] to withdraw stake rewards into Moonlight account. @@ -336,7 +290,7 @@ pub fn moonlight_stake_reward( ) } -/// Create an unproven [`Transaction`] to unstake into a phoenix-note. +/// Create a [`Transaction`] to unstake into a phoenix-note. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -358,49 +312,21 @@ pub fn phoenix_unstake( chain_id: u8, prover: &P, ) -> Result { - let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); - let change_pk = receiver_pk; - - let transfer_value = 0; - let obfuscated_transaction = false; - let deposit = 0; - - // split the input notes and openings from the nullifiers - let mut nullifiers = Vec::with_capacity(inputs.len()); - let inputs = inputs - .into_iter() - .map(|(note, opening, nullifier)| { - nullifiers.push(nullifier); - (note, opening) - }) - .collect(); - - let gas_payment_token = WithdrawReplayToken::Phoenix(nullifiers); - - let contract_call = unstake_to_phoenix( + let utx = unproven::phoenix_unstake( rng, phoenix_sender_sk, stake_sk, - gas_payment_token, - unstake_value, - )?; - - phoenix::( - rng, - phoenix_sender_sk, - &change_pk, - &receiver_pk, inputs, root, - transfer_value, - obfuscated_transaction, - deposit, + unstake_value, gas_limit, gas_price, chain_id, - Some(contract_call), - prover, - ) + )?; + + let proof = prover.prove(&utx.circuit)?; + + Ok(PhoenixTransaction::from_unproven(utx, proof).into()) } /// Create a [`Transaction`] to unstake into a Moonlight account. @@ -449,8 +375,7 @@ pub fn moonlight_unstake( ) } -/// Create an unproven [`Transaction`] to convert Phoenix Dusk into Moonlight -/// Dusk. +/// Create a [`Transaction`] to convert Phoenix Dusk into Moonlight Dusk. /// /// # Note /// The ownership of both sender and receiver keys is required, and @@ -476,49 +401,21 @@ pub fn phoenix_to_moonlight( chain_id: u8, prover: &P, ) -> Result { - let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); - let change_pk = receiver_pk; - - let transfer_value = 0; - let obfuscated_transaction = true; - let deposit = convert_value; // a convertion is a simultaneous deposit to *and* withdrawal from the - // transfer contract - - // split the input notes and openings from the nullifiers - let mut nullifiers = Vec::with_capacity(inputs.len()); - let inputs = inputs - .into_iter() - .map(|(note, opening, nullifier)| { - nullifiers.push(nullifier); - (note, opening) - }) - .collect(); - - let gas_payment_token = WithdrawReplayToken::Phoenix(nullifiers); - - let contract_call = convert_to_moonlight( - rng, - moonlight_receiver_sk, - gas_payment_token, - convert_value, - )?; - - phoenix::( + let utx = unproven::phoenix_to_moonlight( rng, phoenix_sender_sk, - &change_pk, - &receiver_pk, + moonlight_receiver_sk, inputs, root, - transfer_value, - obfuscated_transaction, - deposit, + convert_value, gas_limit, gas_price, chain_id, - Some(contract_call), - prover, - ) + )?; + + let proof = prover.prove(&utx.circuit)?; + + Ok(PhoenixTransaction::from_unproven(utx, proof).into()) } /// Create a [`Transaction`] to convert Moonlight Dusk into Phoenix Dusk. @@ -569,7 +466,7 @@ pub fn moonlight_to_phoenix( ) } -/// Create a new unproven [`Transaction`] to deploy a contract to the network. +/// Create a [`Transaction`] to deploy a contract to the network. /// /// # Errors /// The creation of a transaction is not possible and will error if: @@ -593,50 +490,23 @@ pub fn phoenix_deployment( chain_id: u8, prover: &P, ) -> Result { - let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); - let change_pk = receiver_pk; - - let transfer_value = 0; - let obfuscated_transaction = true; - let deposit = 0; - - // split the input notes and openings from the nullifiers - let mut nullifiers = Vec::with_capacity(inputs.len()); - let inputs = inputs - .into_iter() - .map(|(note, opening, nullifier)| { - nullifiers.push(nullifier); - (note, opening) - }) - .collect(); - - let bytes = bytecode.into(); - let deploy = ContractDeploy { - bytecode: ContractBytecode { - hash: blake3::hash(&bytes).into(), - bytes, - }, - owner: owner.to_bytes().to_vec(), - init_args: Some(init_args), - nonce, - }; - - phoenix( + let utx = unproven::phoenix_deployment( rng, phoenix_sender_sk, - &change_pk, - &receiver_pk, inputs, root, - transfer_value, - obfuscated_transaction, - deposit, + bytecode, + owner, + init_args, + nonce, gas_limit, gas_price, chain_id, - Some(deploy), - prover, - ) + )?; + + let proof = prover.prove(&utx.circuit)?; + + Ok(PhoenixTransaction::from_unproven(utx, proof).into()) } /// Create a new [`Transaction`] to deploy a contract to the network. diff --git a/wallet-core/src/transaction/unproven.rs b/wallet-core/src/transaction/unproven.rs new file mode 100644 index 0000000000..38f20a708a --- /dev/null +++ b/wallet-core/src/transaction/unproven.rs @@ -0,0 +1,392 @@ +// 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. + +//! Implementations of basic wallet functionalities to create unproven phoenix +//! transactions. + +use alloc::vec::Vec; + +use dusk_bytes::Serializable; +use rand::{CryptoRng, RngCore}; + +use execution_core::{ + signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, + stake::{Stake, STAKE_CONTRACT}, + transfer::{ + data::{ + ContractBytecode, ContractCall, ContractDeploy, TransactionData, + }, + phoenix::{ + Note, NoteOpening, PublicKey as PhoenixPublicKey, + SecretKey as PhoenixSecretKey, UnprovenTransaction, + }, + withdraw::WithdrawReplayToken, + }, + BlsScalar, Error, +}; + +/// Create a generic [`UnprovenTransaction`] to be proven at a later +/// point. +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - one of the input-notes doesn't belong to the `phoenix_sender_sk` +/// - the transaction input doesn't cover the transaction costs +/// - the `inputs` vector is either empty or larger than 4 elements +/// - the `inputs` vector contains duplicate `Note`s +/// - the Memo provided with `data` is too large +#[allow(clippy::too_many_arguments)] +pub fn phoenix( + rng: &mut R, + sender_sk: &PhoenixSecretKey, + change_pk: &PhoenixPublicKey, + receiver_pk: &PhoenixPublicKey, + inputs: Vec<(Note, NoteOpening)>, + root: BlsScalar, + transfer_value: u64, + obfuscated_transaction: bool, + deposit: u64, + gas_limit: u64, + gas_price: u64, + chain_id: u8, + data: Option>, +) -> Result { + UnprovenTransaction::new( + rng, + sender_sk, + change_pk, + receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + data, + ) +} + +/// Create an [`UnprovenTransaction`] to stake from phoenix-notes. +/// +/// # Note +/// The `current_nonce` is NOT incremented and should be incremented +/// by the caller of this function, if its not done so, rusk +/// will throw 500 error +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - one of the input-notes doesn't belong to the `phoenix_sender_sk` +/// - the transaction input doesn't cover the transaction costs +/// - the `inputs` vector is either empty or larger than 4 elements +/// - the `inputs` vector contains duplicate `Note`s +#[allow(clippy::too_many_arguments)] +pub fn phoenix_stake( + rng: &mut R, + phoenix_sender_sk: &PhoenixSecretKey, + stake_sk: &BlsSecretKey, + inputs: Vec<(Note, NoteOpening)>, + root: BlsScalar, + gas_limit: u64, + gas_price: u64, + chain_id: u8, + stake_value: u64, + current_nonce: u64, +) -> Result { + let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); + let change_pk = receiver_pk; + + let transfer_value = 0; + let obfuscated_transaction = false; + let deposit = stake_value; + + let stake = Stake::new(stake_sk, stake_value, current_nonce, chain_id); + + let contract_call = ContractCall::new(STAKE_CONTRACT, "stake", &stake)?; + + phoenix( + rng, + phoenix_sender_sk, + &change_pk, + &receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(contract_call), + ) +} + +/// Create an [`UnprovenTransaction`] to withdraw stake rewards into a +/// phoenix-note. +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - one of the input-notes doesn't belong to the `phoenix_sender_sk` +/// - the transaction input doesn't cover the transaction costs +/// - the `inputs` vector is either empty or larger than 4 elements +/// - the `inputs` vector contains duplicate `Note`s +#[allow(clippy::too_many_arguments)] +pub fn phoenix_stake_reward( + rng: &mut R, + phoenix_sender_sk: &PhoenixSecretKey, + stake_sk: &BlsSecretKey, + inputs: Vec<(Note, NoteOpening, BlsScalar)>, + root: BlsScalar, + reward_amount: u64, + gas_limit: u64, + gas_price: u64, + chain_id: u8, +) -> Result { + let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); + let change_pk = receiver_pk; + + let transfer_value = 0; + let obfuscated_transaction = false; + let deposit = 0; + + // split the input notes and openings from the nullifiers + let mut nullifiers = Vec::with_capacity(inputs.len()); + let inputs = inputs + .into_iter() + .map(|(note, opening, nullifier)| { + nullifiers.push(nullifier); + (note, opening) + }) + .collect(); + + let gas_payment_token = WithdrawReplayToken::Phoenix(nullifiers); + + let contract_call = super::stake_reward_to_phoenix( + rng, + phoenix_sender_sk, + stake_sk, + gas_payment_token, + reward_amount, + )?; + + phoenix( + rng, + phoenix_sender_sk, + &change_pk, + &receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(contract_call), + ) +} + +/// Create an [`UnprovenTransaction`] to unstake into a phoenix-note. +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - one of the input-notes doesn't belong to the `sender_sk` +/// - the transaction input doesn't cover the transaction costs +/// - the `inputs` vector is either empty or larger than 4 elements +/// - the `inputs` vector contains duplicate `Note`s +#[allow(clippy::too_many_arguments)] +pub fn phoenix_unstake( + rng: &mut R, + phoenix_sender_sk: &PhoenixSecretKey, + stake_sk: &BlsSecretKey, + inputs: Vec<(Note, NoteOpening, BlsScalar)>, + root: BlsScalar, + unstake_value: u64, + gas_limit: u64, + gas_price: u64, + chain_id: u8, +) -> Result { + let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); + let change_pk = receiver_pk; + + let transfer_value = 0; + let obfuscated_transaction = false; + let deposit = 0; + + // split the input notes and openings from the nullifiers + let mut nullifiers = Vec::with_capacity(inputs.len()); + let inputs = inputs + .into_iter() + .map(|(note, opening, nullifier)| { + nullifiers.push(nullifier); + (note, opening) + }) + .collect(); + + let gas_payment_token = WithdrawReplayToken::Phoenix(nullifiers); + + let contract_call = super::unstake_to_phoenix( + rng, + phoenix_sender_sk, + stake_sk, + gas_payment_token, + unstake_value, + )?; + + phoenix( + rng, + phoenix_sender_sk, + &change_pk, + &receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(contract_call), + ) +} + +/// Create an [`UnprovenTransaction`] to convert Phoenix Dusk into Moonlight +/// Dusk. +/// +/// # Note +/// The ownership of both sender and receiver keys is required, and +/// enforced by the protocol. +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - one of the input-notes doesn't belong to the `sender_sk` +/// - the transaction input doesn't cover the transaction costs +/// - the `inputs` vector is either empty or larger than 4 elements +/// - the `inputs` vector contains duplicate `Note`s +#[allow(clippy::too_many_arguments)] +pub fn phoenix_to_moonlight( + rng: &mut R, + phoenix_sender_sk: &PhoenixSecretKey, + moonlight_receiver_sk: &BlsSecretKey, + inputs: Vec<(Note, NoteOpening, BlsScalar)>, + root: BlsScalar, + convert_value: u64, + gas_limit: u64, + gas_price: u64, + chain_id: u8, +) -> Result { + let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); + let change_pk = receiver_pk; + + let transfer_value = 0; + let obfuscated_transaction = true; + let deposit = convert_value; // a convertion is a simultaneous deposit to *and* withdrawal from the + // transfer contract + + // split the input notes and openings from the nullifiers + let mut nullifiers = Vec::with_capacity(inputs.len()); + let inputs = inputs + .into_iter() + .map(|(note, opening, nullifier)| { + nullifiers.push(nullifier); + (note, opening) + }) + .collect(); + + let gas_payment_token = WithdrawReplayToken::Phoenix(nullifiers); + + let contract_call = super::convert_to_moonlight( + rng, + moonlight_receiver_sk, + gas_payment_token, + convert_value, + )?; + + phoenix( + rng, + phoenix_sender_sk, + &change_pk, + &receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(contract_call), + ) +} + +/// Create a new [`UnprovenTransaction`] to deploy a contract to the network. +/// +/// # Errors +/// The creation of a transaction is not possible and will error if: +/// - one of the input-notes doesn't belong to the `sender_sk` +/// - the transaction input doesn't cover the transaction costs +/// - the `inputs` vector is either empty or larger than 4 elements +/// - the `inputs` vector contains duplicate `Note`s +/// - the `Prove` trait is implemented incorrectly +#[allow(clippy::too_many_arguments)] +pub fn phoenix_deployment( + rng: &mut R, + phoenix_sender_sk: &PhoenixSecretKey, + inputs: Vec<(Note, NoteOpening, BlsScalar)>, + root: BlsScalar, + bytecode: impl Into>, + owner: &BlsPublicKey, + init_args: Vec, + nonce: u64, + gas_limit: u64, + gas_price: u64, + chain_id: u8, +) -> Result { + let receiver_pk = PhoenixPublicKey::from(phoenix_sender_sk); + let change_pk = receiver_pk; + + let transfer_value = 0; + let obfuscated_transaction = true; + let deposit = 0; + + // split the input notes and openings from the nullifiers + let mut nullifiers = Vec::with_capacity(inputs.len()); + let inputs = inputs + .into_iter() + .map(|(note, opening, nullifier)| { + nullifiers.push(nullifier); + (note, opening) + }) + .collect(); + + let bytes = bytecode.into(); + let deploy = ContractDeploy { + bytecode: ContractBytecode { + hash: blake3::hash(&bytes).into(), + bytes, + }, + owner: owner.to_bytes().to_vec(), + init_args: Some(init_args), + nonce, + }; + + phoenix( + rng, + phoenix_sender_sk, + &change_pk, + &receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(deploy), + ) +} From fab8c19d9803a19d1d0e405a0a59125a6235359c Mon Sep 17 00:00:00 2001 From: moana Date: Mon, 16 Sep 2024 18:22:35 +0200 Subject: [PATCH 3/3] rusk-wallet: Adjust to added utx support in wallet-core --- rusk-wallet/src/clients.rs | 56 +++++++++++++---------------- rusk-wallet/src/wallet.rs | 73 ++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 67 deletions(-) diff --git a/rusk-wallet/src/clients.rs b/rusk-wallet/src/clients.rs index 748a118205..b2af9b16eb 100644 --- a/rusk-wallet/src/clients.rs +++ b/rusk-wallet/src/clients.rs @@ -11,7 +11,10 @@ use execution_core::{ signatures::bls::PublicKey as AccountPublicKey, transfer::{ moonlight::AccountData, - phoenix::{Note, NoteLeaf, Prove}, + phoenix::{ + Note, NoteLeaf, Transaction as PhoenixTransaction, + UnprovenTransaction, + }, Transaction, }, Error as ExecutionCoreError, @@ -56,20 +59,6 @@ const SYNC_INTERVAL_SECONDS: u64 = 3; /// SIZE of the tree leaf pub const TREE_LEAF: usize = std::mem::size_of::(); -/// A prover struct that has the `Prove` trait from executio-core implemented. -/// It currently uses a hardcoded prover which delegates the proving to the -/// `prove_execute` -pub struct Prover; - -impl Prove for Prover { - fn prove( - &self, - tx_circuit_vec_bytes: &[u8], - ) -> Result, ExecutionCoreError> { - Ok(tx_circuit_vec_bytes.to_vec()) - } -} - /// The state struct is responsible for managing the state of the wallet pub struct State { cache: Mutex>, @@ -152,33 +141,36 @@ impl State { sync_db(&self.client, &self.cache(), &self.store, self.status).await } - /// Requests that a node prove the given transaction and later propagates it - /// Skips writing the proof for non phoenix transactions - pub async fn prove_and_propagate( + /// Requests that a node prove the given phoenix-transaction. + pub async fn prove_unproven( &self, - tx: Transaction, + utx: UnprovenTransaction, ) -> Result { let status = self.status; let prover = &self.prover; - let mut tx = tx; - if let Transaction::Phoenix(utx) = &mut tx { - let status = self.status; - let proof = utx.proof(); + status("Attempt to prove unproven tx..."); - status("Attempt to prove tx..."); + let prove_req = RuskRequest::new("prove_execute", utx.circuit.clone()); - let prove_req = RuskRequest::new("prove_execute", proof.to_vec()); + let proof = prover + .call(2, "rusk", &prove_req) + .await + .map_err(|e| ExecutionCoreError::PhoenixCircuit(e.to_string()))?; - let proof = - prover.call(2, "rusk", &prove_req).await.map_err(|e| { - ExecutionCoreError::PhoenixCircuit(e.to_string()) - })?; + let tx = PhoenixTransaction::from_unproven(utx, proof); - utx.set_proof(proof); + status("Proving sucesss!"); - status("Proving sucesss!"); - } + Ok(tx.into()) + } + + /// Requests that a node propagates a given transaction. + pub async fn propagate( + &self, + tx: Transaction, + ) -> Result { + let status = self.status; let tx_bytes = tx.to_var_bytes(); diff --git a/rusk-wallet/src/wallet.rs b/rusk-wallet/src/wallet.rs index efaf52611c..f4ba9c9a52 100644 --- a/rusk-wallet/src/wallet.rs +++ b/rusk-wallet/src/wallet.rs @@ -21,9 +21,6 @@ use serde::Serialize; use std::fmt::Debug; use std::fs; use std::path::{Path, PathBuf}; -use wallet_core::transaction::{ - moonlight_deployment, moonlight_stake_reward, phoenix_deployment, -}; use wallet_core::{ phoenix_balance, @@ -32,9 +29,12 @@ use wallet_core::{ derive_phoenix_vk, }, transaction::{ - moonlight, moonlight_stake, moonlight_to_phoenix, moonlight_unstake, - phoenix, phoenix_stake, phoenix_stake_reward, phoenix_to_moonlight, - phoenix_unstake, + moonlight, moonlight_deployment, moonlight_stake, + moonlight_stake_reward, moonlight_to_phoenix, moonlight_unstake, + unproven::{ + phoenix, phoenix_deployment, phoenix_stake, phoenix_stake_reward, + phoenix_to_moonlight, phoenix_unstake, + }, }, BalanceInfo, }; @@ -52,7 +52,7 @@ use zeroize::Zeroize; use super::*; use crate::{ - clients::{Prover, State}, + clients::State, crypto::encrypt, currency::Dusk, dat::{ @@ -497,7 +497,7 @@ impl Wallet { from_sk.zeroize(); - state.prove_and_propagate(tx).await + state.propagate(tx).await } /// Executes a generic contract call, paying gas with phoenix notes @@ -537,7 +537,7 @@ impl Wallet { let root = state.fetch_root().await?; let chain_id = state.fetch_chain_id().await?; - let tx = phoenix( + let utx = phoenix( &mut rng, &sender_sk, sender.pk()?, @@ -551,12 +551,12 @@ impl Wallet { gas.price, chain_id, Some(data), - &Prover, )?; sender_sk.zeroize(); - state.prove_and_propagate(tx).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Transfers funds between Phoenix addresses @@ -600,7 +600,7 @@ impl Wallet { let root = state.fetch_root().await?; let chain_id = state.fetch_chain_id().await?; - let tx = phoenix( + let utx = phoenix( &mut rng, &sender_sk, change_pk, @@ -614,12 +614,12 @@ impl Wallet { gas.price, chain_id, None::, - &Prover, )?; sender_sk.zeroize(); - state.prove_and_propagate(tx).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Transfer through Moonlight @@ -668,7 +668,7 @@ impl Wallet { from_sk.zeroize(); - state.prove_and_propagate(tx).await + state.propagate(tx).await } /// Stakes Dusk using Phoenix notes @@ -716,15 +716,16 @@ impl Wallet { let root = state.fetch_root().await?; let chain_id = state.fetch_chain_id().await?; - let stake = phoenix_stake( + let utx = phoenix_stake( &mut rng, &sender_sk, &stake_sk, inputs, root, gas.limit, - gas.price, chain_id, amt, nonce, &Prover, + gas.price, chain_id, amt, nonce, )?; sender_sk.zeroize(); stake_sk.zeroize(); - state.prove_and_propagate(stake).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Stake via Moonlight @@ -771,7 +772,7 @@ impl Wallet { stake_sk.zeroize(); - state.prove_and_propagate(stake).await + state.propagate(stake).await } /// Obtains stake information for a given address @@ -815,7 +816,7 @@ impl Wallet { let root = state.fetch_root().await?; let chain_id = state.fetch_chain_id().await?; - let unstake = phoenix_unstake( + let utx = phoenix_unstake( &mut rng, &sender_sk, &stake_sk, @@ -825,13 +826,13 @@ impl Wallet { gas.limit, gas.price, chain_id, - &Prover, )?; sender_sk.zeroize(); stake_sk.zeroize(); - state.prove_and_propagate(unstake).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Unstakes Dusk through Moonlight @@ -875,7 +876,7 @@ impl Wallet { stake_sk.zeroize(); - state.prove_and_propagate(unstake).await + state.propagate(unstake).await } /// Withdraw accumulated staking reward for a given address to Phoenix @@ -907,7 +908,7 @@ impl Wallet { .map(|s| s.reward) .unwrap_or(0); - let withdraw = phoenix_stake_reward( + let utx = phoenix_stake_reward( &mut rng, &sender_sk, &stake_sk, @@ -917,13 +918,13 @@ impl Wallet { gas.limit, gas.price, chain_id, - &Prover, )?; sender_sk.zeroize(); stake_sk.zeroize(); - state.prove_and_propagate(withdraw).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Convert balance from Phoenix to Moonlight @@ -947,15 +948,16 @@ impl Wallet { let mut sender_sk = self.phoenix_secret_key(sender_index); let mut stake_sk = self.bls_secret_key(sender_index); - let convert = phoenix_to_moonlight( + let utx = phoenix_to_moonlight( &mut rng, &sender_sk, &stake_sk, inputs, root, amt, gas.limit, - gas.price, chain_id, &Prover, + gas.price, chain_id, )?; sender_sk.zeroize(); stake_sk.zeroize(); - state.prove_and_propagate(convert).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Convert balance from Moonlight to Phoenix @@ -985,7 +987,7 @@ impl Wallet { sender_sk.zeroize(); stake_sk.zeroize(); - state.prove_and_propagate(convert).await + state.propagate(convert).await } /// Withdraw accumulated staking reward for a given address to Moonlight @@ -1011,7 +1013,7 @@ impl Wallet { sender_sk.zeroize(); - state.prove_and_propagate(withdraw).await + state.propagate(withdraw).await } /// Deploy a contract using Moonlight @@ -1037,7 +1039,7 @@ impl Wallet { sender_sk.zeroize(); - state.prove_and_propagate(deploy).await + state.propagate(deploy).await } /// Deploy a contract using Phoenix @@ -1060,14 +1062,15 @@ impl Wallet { let mut sender_sk = self.phoenix_secret_key(sender_index); let apk = self.bls_public_key(sender_index); - let deploy = phoenix_deployment( + let utx = phoenix_deployment( &mut rng, &sender_sk, inputs, root, bytes_code, &apk, init_args, 0, - gas.limit, gas.price, chain_id, &Prover, + gas.limit, gas.price, chain_id, )?; sender_sk.zeroize(); - state.prove_and_propagate(deploy).await + let tx = state.prove_unproven(utx).await?; + state.propagate(tx).await } /// Returns BLS key-pair for provisioner nodes