From 435aacd85bca9e7590fc3b356960f78b2fe7cb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Wed, 11 Sep 2024 16:27:12 +0200 Subject: [PATCH 1/4] wallet-core: add deployment transaction frunctions - `phoenix_deployment` - `moonlight_deployment` --- wallet-core/Cargo.toml | 1 + wallet-core/src/transaction.rs | 126 ++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/wallet-core/Cargo.toml b/wallet-core/Cargo.toml index 6e4d904167..eaa96b60a6 100644 --- a/wallet-core/Cargo.toml +++ b/wallet-core/Cargo.toml @@ -11,6 +11,7 @@ dusk-bytes = "0.1" bytecheck = { version = "0.6", default-features = false } zeroize = { version = "1", default-features = false, features = ["derive"] } rand_chacha = { version = "0.3", default-features = false } +blake3 = { version = "1", default-features = false } sha2 = { version = "0.10", default-features = false } rand = { version = "0.8", default-features = false } ff = { version = "0.13", default-features = false } diff --git a/wallet-core/src/transaction.rs b/wallet-core/src/transaction.rs index 6f449ccd71..91afa02cc2 100644 --- a/wallet-core/src/transaction.rs +++ b/wallet-core/src/transaction.rs @@ -8,16 +8,18 @@ use alloc::vec::Vec; -use rand::{CryptoRng, RngCore}; - +use dusk_bytes::Serializable; use ff::Field; +use rand::{CryptoRng, RngCore}; use zeroize::Zeroize; use execution_core::{ signatures::bls::{PublicKey as BlsPublicKey, SecretKey as BlsSecretKey}, stake::{Stake, Withdraw as StakeWithdraw, STAKE_CONTRACT}, transfer::{ - data::{ContractCall, TransactionData}, + data::{ + ContractBytecode, ContractCall, ContractDeploy, TransactionData, + }, moonlight::Transaction as MoonlightTransaction, phoenix::{ Note, NoteOpening, Prove, PublicKey as PhoenixPublicKey, @@ -567,6 +569,124 @@ pub fn moonlight_to_phoenix( ) } +/// Create a new unproven [`Transaction`] 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, + 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( + rng, + phoenix_sender_sk, + &change_pk, + &receiver_pk, + inputs, + root, + transfer_value, + obfuscated_transaction, + deposit, + gas_limit, + gas_price, + chain_id, + Some(deploy), + prover, + ) +} + +/// Create a new [`Transaction`] to deploy a contract to the network. +/// +/// # 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 this transaction doesn't error, but still returns a result +/// for the sake of API consistency. +#[allow(clippy::too_many_arguments)] +pub fn moonlight_deployment( + moonlight_sender_sk: &BlsSecretKey, + bytecode: impl Into>, + owner: &BlsPublicKey, + init_args: Vec, + gas_limit: u64, + gas_price: u64, + moonlight_current_nonce: u64, + deploy_nonce: u64, + chain_id: u8, +) -> Result { + let transfer_value = 0; + let deposit = 0; + + 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: deploy_nonce, + }; + + moonlight( + moonlight_sender_sk, + None, + transfer_value, + deposit, + gas_limit, + gas_price, + moonlight_current_nonce, + chain_id, + Some(deploy), + ) +} + fn stake_reward_to_phoenix( rng: &mut R, phoenix_sender_sk: &PhoenixSecretKey, From acc1b295bfd97030b8552cfd7f96172c3efd72e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Wed, 11 Sep 2024 17:36:20 +0200 Subject: [PATCH 2/4] execution-core: adjust comment on deployment hash --- execution-core/src/transfer/data.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execution-core/src/transfer/data.rs b/execution-core/src/transfer/data.rs index 20d77e13e2..2a2cf99bb6 100644 --- a/execution-core/src/transfer/data.rs +++ b/execution-core/src/transfer/data.rs @@ -188,7 +188,7 @@ impl ContractCall { #[archive_attr(derive(CheckBytes))] /// Holds bytes of bytecode and its hash. pub struct ContractBytecode { - /// Hash of the bytecode bytes. + /// Blake3 hash of the bytecode bytes. pub hash: [u8; 32], /// Bytecode bytes. pub bytes: Vec, From 93c0e18a7efd9dcc3a62f840d4888bff803c04a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Wed, 11 Sep 2024 17:36:46 +0200 Subject: [PATCH 3/4] test-wallet: add deployment functions --- test-wallet/src/imp.rs | 91 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/test-wallet/src/imp.rs b/test-wallet/src/imp.rs index ecdf874d04..ce3a0d89e6 100644 --- a/test-wallet/src/imp.rs +++ b/test-wallet/src/imp.rs @@ -38,8 +38,9 @@ use wallet_core::{ keys::{derive_bls_sk, derive_phoenix_sk}, phoenix_balance, transaction::{ - moonlight_stake, moonlight_stake_reward, moonlight_to_phoenix, - moonlight_unstake, phoenix as phoenix_transaction, phoenix_stake, + moonlight_deployment, moonlight_stake, moonlight_stake_reward, + moonlight_to_phoenix, moonlight_unstake, + phoenix as phoenix_transaction, phoenix_deployment, phoenix_stake, phoenix_stake_reward, phoenix_to_moonlight, phoenix_unstake, }, BalanceInfo, @@ -647,6 +648,50 @@ where Ok(tx) } + /// Deploy a contract using Phoenix to pay for gas. + pub fn phoenix_deployment( + &self, + rng: &mut Rng, + sender_index: u8, + bytecode: impl Into>, + owner: &BlsPublicKey, + init_args: Vec, + nonce: u64, + gas_limit: u64, + gas_price: u64, + ) -> Result> { + let mut phoenix_sender_sk = self.phoenix_secret_key(sender_index)?; + + let inputs = self.input_notes_openings_nullifiers( + &phoenix_sender_sk, + gas_limit * gas_price, + )?; + + let root = self.state.fetch_root().map_err(Error::from_state_err)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let tx = phoenix_deployment( + rng, + &phoenix_sender_sk, + inputs, + root, + bytecode, + owner, + init_args, + nonce, + gas_limit, + gas_price, + chain_id, + &LocalProver, + )?; + + phoenix_sender_sk.zeroize(); + + Ok(tx) + } + /// Transfer Dusk from one account to another using moonlight. pub fn moonlight_transfer( &self, @@ -898,6 +943,48 @@ where Ok(tx) } + /// Deploy a contract using Moonlight to pay for gas. + pub fn moonlight_deployment( + &self, + sender_index: u8, + bytecode: impl Into>, + owner: &BlsPublicKey, + init_args: Vec, + gas_limit: u64, + gas_price: u64, + deploy_nonce: u64, + ) -> Result> { + let mut sender_sk = self.account_secret_key(sender_index)?; + let sender_pk = self.account_public_key(sender_index)?; + + let chain_id = + self.state.fetch_chain_id().map_err(Error::from_state_err)?; + + let moonlight_current_nonce = self + .state + .fetch_account(&sender_pk) + .map_err(Error::from_state_err)? + .nonce; + + let moonlight_nonce = moonlight_current_nonce + 1; + + let tx = moonlight_deployment( + &sender_sk, + bytecode, + owner, + init_args, + gas_limit, + gas_price, + moonlight_nonce, + deploy_nonce, + chain_id, + )?; + + sender_sk.zeroize(); + + Ok(tx) + } + /// Gets the balance of a key. pub fn get_balance( &self, From d9ca75f76e6b0b2e5dd4ad1c9e56128d13914529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Leegwater=20Sim=C3=B5es?= Date: Wed, 11 Sep 2024 19:31:15 +0200 Subject: [PATCH 4/4] rusk: add block gas limit test on deployments We add a test that, much like the `multi_transfer` test, runs out of gas in a block on the last transaction, but in this case that transaction is a contract deployment. Called `multi_transfer_deploy`, this should show that running out of gas in a block during a deployment doesn't result in a hash mismatch between EST and AST. --- rusk/tests/services/multi_transfer.rs | 206 ++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) diff --git a/rusk/tests/services/multi_transfer.rs b/rusk/tests/services/multi_transfer.rs index f3cf71cc56..803ce108a7 100644 --- a/rusk/tests/services/multi_transfer.rs +++ b/rusk/tests/services/multi_transfer.rs @@ -26,6 +26,10 @@ const BLOCK_GAS_LIMIT: u64 = 24_000_000; const GAS_LIMIT: u64 = 12_000_000; // Lowest value for a transfer const INITIAL_BALANCE: u64 = 10_000_000_000; +const BOB_BYTECODE: &[u8] = include_bytes!( + "../../../target/dusk/wasm32-unknown-unknown/release/bob.wasm" +); + // Creates the Rusk initial state for the tests below fn initial_state>(dir: P) -> Result { let snapshot = @@ -175,6 +179,167 @@ fn wallet_transfer( ); } +/// Executes three different transactions in the same block, expecting only two +/// to be included due to exceeding the block gas limit. The last of the +/// transactions is a contract deployment, and is expected to deploy that +/// contract, but then be reverted. +fn wallet_transfer_deploy( + rusk: &Rusk, + wallet: &wallet::Wallet, + amount: u64, +) { + // Generate a receiver pk + let receiver = wallet + .phoenix_public_key(3) + .expect("Failed to get public key"); + + let mut rng = StdRng::seed_from_u64(0xdead); + + let initial_balance_0 = wallet + .get_balance(0) + .expect("Failed to get the balance") + .value; + let initial_balance_1 = wallet + .get_balance(1) + .expect("Failed to get the balance") + .value; + let initial_balance_2 = wallet + .get_balance(2) + .expect("Failed to get the balance") + .value; + + // Check the senders initial balance is correct + assert_eq!( + initial_balance_0, INITIAL_BALANCE, + "Wrong initial balance for the sender" + ); + assert_eq!( + initial_balance_1, INITIAL_BALANCE, + "Wrong initial balance for the sender" + ); + assert_eq!( + initial_balance_2, INITIAL_BALANCE, + "Wrong initial balance for the sender" + ); + + // Check the receiver initial balance is zero + assert_eq!( + wallet + .get_balance(3) + .expect("Failed to get the balance") + .value, + 0, + "Wrong initial balance for the receiver" + ); + + let mut txs = Vec::with_capacity(3); + + for i in 0..2 { + let tx = wallet + .phoenix_transfer(&mut rng, i, &receiver, amount, GAS_LIMIT, 1) + .expect("Failed to transfer"); + txs.push(tx); + } + + let init_args = Vec::new(); + let owner = wallet + .account_public_key(0) + .expect("Gettitng public key should work"); + + let tx = wallet + .phoenix_deployment( + &mut rng, + 2, + BOB_BYTECODE, + &owner, + init_args, + 0, + GAS_LIMIT, + 1, + ) + .expect("Failed to deploy"); + txs.push(tx); + + let expected = ExecuteResult { + discarded: 0, + executed: 2, + }; + + generator_procedure( + rusk, + &txs[..], + BLOCK_HEIGHT, + BLOCK_GAS_LIMIT, + vec![], + Some(expected), + ) + .expect("generator procedure to succeed"); + + // Check the receiver's balance is changed accordingly + assert_eq!( + wallet + .get_balance(3) + .expect("Failed to get the balance") + .value, + 2 * amount, + "Wrong resulting balance for the receiver" + ); + + let final_balance_0 = wallet + .get_balance(0) + .expect("Failed to get the balance") + .value; + let gas_limit_0 = txs[0].gas_limit(); + let gas_price_0 = txs[0].gas_price(); + let fee_0 = gas_limit_0 * gas_price_0; + + let final_balance_1 = wallet + .get_balance(1) + .expect("Failed to get the balance") + .value; + let gas_limit_1 = txs[1].gas_limit(); + let gas_price_1 = txs[1].gas_price(); + let fee_1 = gas_limit_1 * gas_price_1; + + assert!( + initial_balance_0 - amount - fee_0 <= final_balance_0, + "Final sender balance {} should be greater or equal than {}", + final_balance_0, + initial_balance_0 - amount - fee_0 + ); + + assert!( + initial_balance_0 - amount >= final_balance_0, + "Final sender balance {} should be lesser or equal than {}", + final_balance_0, + initial_balance_0 - amount + ); + + assert!( + initial_balance_1 - amount - fee_1 <= final_balance_1, + "Final sender balance {} should be greater or equal than {}", + final_balance_1, + initial_balance_1 - amount - fee_1 + ); + + assert!( + initial_balance_1 - amount >= final_balance_1, + "Final sender balance {} should be lesser or equal than {}", + final_balance_1, + initial_balance_1 - amount + ); + + // Check the discarded transaction didn't change the balance + assert_eq!( + wallet + .get_balance(2) + .expect("Failed to get the balance") + .value, + initial_balance_2, + "Wrong resulting balance for discarded TX sender" + ); +} + #[tokio::test(flavor = "multi_thread")] pub async fn multi_transfer() -> Result<()> { // Setup the logger @@ -215,3 +380,44 @@ pub async fn multi_transfer() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +pub async fn multi_transfer_deploy() -> Result<()> { + // Setup the logger + logger(); + + let tmp = tempdir().expect("Should be able to create temporary directory"); + let rusk = initial_state(&tmp)?; + + let cache = Arc::new(RwLock::new(HashMap::new())); + + // Create a wallet + let wallet = wallet::Wallet::new( + TestStore, + TestStateClient { + rusk: rusk.clone(), + cache, + }, + ); + + let original_root = rusk.state_root(); + + info!("Original Root: {:?}", hex::encode(original_root)); + + wallet_transfer_deploy(&rusk, &wallet, 1_000); + + // Check the state's root is changed from the original one + let new_root = rusk.state_root(); + info!( + "New root after the 1st transfer: {:?}", + hex::encode(new_root) + ); + assert_ne!(original_root, new_root, "Root should have changed"); + + // let recv = kadcast_recv.try_recv(); + // let (_, _, h) = recv.expect("Transaction has not been locally + // propagated"); assert_eq!(h, 0, "Transaction locally propagated with + // wrong height"); + + Ok(()) +}