From 092129eb37dcdc8a4559d3c4c6a74d4ce1e836d4 Mon Sep 17 00:00:00 2001 From: moana Date: Sat, 21 Dec 2024 17:04:16 +0100 Subject: [PATCH 1/6] vm: Add transaction execution function --- vm/src/lib.rs | 33 ++------ vm/src/session.rs | 196 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 28 deletions(-) create mode 100644 vm/src/session.rs diff --git a/vm/src/lib.rs b/vm/src/lib.rs index 4de346889..6c3bfdd43 100644 --- a/vm/src/lib.rs +++ b/vm/src/lib.rs @@ -23,7 +23,7 @@ use std::fmt::{self, Debug, Formatter}; use std::path::{Path, PathBuf}; use std::thread; -use dusk_core::abi::{Metadata, Query}; +use dusk_core::abi::Query; use piecrust::{SessionData, VM as PiecrustVM}; use self::host_queries::{ @@ -33,33 +33,10 @@ use self::host_queries::{ pub(crate) mod cache; pub mod host_queries; - -/// Create a new session based on the given `VM`. -pub fn new_session( - vm: &VM, - base: [u8; 32], - chain_id: u8, - block_height: u64, -) -> Result { - vm.session( - SessionData::builder() - .base(base) - .insert(Metadata::CHAIN_ID, chain_id)? - .insert(Metadata::BLOCK_HEIGHT, block_height)?, - ) -} - -/// Create a new genesis session based on the given [`VM`]. -pub fn new_genesis_session(vm: &VM, chain_id: u8) -> Session { - vm.session( - SessionData::builder() - .insert(Metadata::CHAIN_ID, chain_id) - .expect("Inserting chain ID in metadata should succeed") - .insert(Metadata::BLOCK_HEIGHT, 0) - .expect("Inserting block height in metadata should succeed"), - ) - .expect("Creating a genesis session should always succeed") -} +pub(crate) mod session; +pub use session::{ + execute, genesis as new_genesis_session, new as new_session, +}; /// Dusk VM is a [`PiecrustVM`] enriched with the host functions specified in /// Dusk's ABI. diff --git a/vm/src/session.rs b/vm/src/session.rs new file mode 100644 index 000000000..078a7eaa2 --- /dev/null +++ b/vm/src/session.rs @@ -0,0 +1,196 @@ +// 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 dusk_core::abi::{gen_contract_id, ContractError, Metadata}; +use dusk_core::transfer::{Transaction, TRANSFER_CONTRACT}; +use piecrust::{CallReceipt, Error, Session, SessionData}; + +use crate::VM; + +/// Create a new session based on the given `VM`. +pub fn new( + vm: &VM, + base: [u8; 32], + chain_id: u8, + block_height: u64, +) -> Result { + vm.session( + SessionData::builder() + .base(base) + .insert(Metadata::CHAIN_ID, chain_id)? + .insert(Metadata::BLOCK_HEIGHT, block_height)?, + ) +} + +/// Create a new genesis session based on the given [`VM`]. +pub fn genesis(vm: &VM, chain_id: u8) -> Session { + vm.session( + SessionData::builder() + .insert(Metadata::CHAIN_ID, chain_id) + .expect("Inserting chain ID in metadata should succeed") + .insert(Metadata::BLOCK_HEIGHT, 0) + .expect("Inserting block height in metadata should succeed"), + ) + .expect("Creating a genesis session should always succeed") +} + +/// Executes a transaction, returning the receipt of the call and the gas spent. +/// The following steps are performed: +/// +/// 1. Check if the transaction contains contract deployment data, and if so, +/// verifies if gas limit is enough for deployment and if the gas price is +/// sufficient for deployment. If either gas price or gas limit is not +/// sufficient for deployment, transaction is discarded. +/// +/// 2. Call the "spend_and_execute" function on the transfer contract with +/// unlimited gas. If this fails, an error is returned. If an error is +/// returned the transaction should be considered unspendable/invalid, but no +/// re-execution of previous transactions is required. +/// +/// 3. If the transaction contains contract deployment data, additional checks +/// are performed and if they pass, deployment is executed. The following +/// checks are performed: +/// - gas limit should be is smaller than deploy charge plus gas used for +/// spending funds +/// - transaction's bytecode's bytes are consistent with bytecode's hash +/// Deployment execution may fail for deployment-specific reasons, such as +/// for example: +/// - contract already deployed +/// - corrupted bytecode +/// If deployment execution fails, the entire gas limit is consumed and error +/// is returned. +/// +/// 4. Call the "refund" function on the transfer contract with unlimited gas. +/// The amount charged depends on the gas spent by the transaction, and the +/// optional contract call in steps 2 or 3. +/// +/// Note that deployment transaction will never be re-executed for reasons +/// related to deployment, as it is either discarded or it charges the +/// full gas limit. It might be re-executed only if some other transaction +/// failed to fit the block. +pub fn execute( + session: &mut Session, + tx: &Transaction, + gas_per_deploy_byte: u64, + min_deploy_points: u64, + min_deploy_gas_price: u64, +) -> Result, ContractError>>, Error> { + // Transaction will be discarded if it is a deployment transaction + // with gas limit smaller than deploy charge. + deploy_check(tx, gas_per_deploy_byte, min_deploy_gas_price)?; + + // Spend the inputs and execute the call. If this errors the transaction is + // unspendable. + let mut receipt = session.call::<_, Result, ContractError>>( + TRANSFER_CONTRACT, + "spend_and_execute", + tx.strip_off_bytecode().as_ref().unwrap_or(tx), + tx.gas_limit(), + )?; + + // Deploy if this is a deployment transaction and spend part is successful. + contract_deploy( + session, + tx, + gas_per_deploy_byte, + min_deploy_points, + &mut receipt, + ); + + // Ensure all gas is consumed if there's an error in the contract call + if receipt.data.is_err() { + receipt.gas_spent = receipt.gas_limit; + } + + // Refund the appropriate amount to the transaction. This call is guaranteed + // to never error. If it does, then a programming error has occurred. As + // such, the call to `Result::expect` is warranted. + let refund_receipt = session + .call::<_, ()>( + TRANSFER_CONTRACT, + "refund", + &receipt.gas_spent, + u64::MAX, + ) + .expect("Refunding must succeed"); + + receipt.events.extend(refund_receipt.events); + + Ok(receipt) +} + +fn deploy_check( + tx: &Transaction, + gas_per_deploy_byte: u64, + min_deploy_gas_price: u64, +) -> Result<(), Error> { + if tx.deploy().is_some() { + let deploy_charge = + tx.deploy_charge(gas_per_deploy_byte, min_deploy_gas_price); + + if tx.gas_price() < min_deploy_gas_price { + return Err(Error::Panic("gas price too low to deploy".into())); + } + if tx.gas_limit() < deploy_charge { + return Err(Error::Panic("not enough gas to deploy".into())); + } + } + + Ok(()) +} + +// Contract deployment will fail and charge full gas limit in the +// following cases: +// 1) Transaction gas limit is smaller than deploy charge plus gas used for +// spending funds. +// 2) Transaction's bytecode's bytes are not consistent with bytecode's hash. +// 3) Deployment fails for deploy-specific reasons like e.g.: +// - contract already deployed +// - corrupted bytecode +// - sufficient gas to spend funds yet insufficient for deployment +fn contract_deploy( + session: &mut Session, + tx: &Transaction, + gas_per_deploy_byte: u64, + min_deploy_points: u64, + receipt: &mut CallReceipt, ContractError>>, +) { + if let Some(deploy) = tx.deploy() { + let gas_left = tx.gas_limit() - receipt.gas_spent; + if receipt.data.is_ok() { + let deploy_charge = + tx.deploy_charge(gas_per_deploy_byte, min_deploy_points); + let min_gas_limit = receipt.gas_spent + deploy_charge; + if gas_left < min_gas_limit { + receipt.data = Err(ContractError::OutOfGas); + } else if !deploy.bytecode.verify_hash() { + receipt.data = Err(ContractError::Panic( + "failed bytecode hash check".into(), + )) + } else { + let result = session.deploy_raw( + Some(gen_contract_id( + &deploy.bytecode.bytes, + deploy.nonce, + &deploy.owner, + )), + deploy.bytecode.bytes.as_slice(), + deploy.init_args.clone(), + deploy.owner.clone(), + gas_left, + ); + match result { + // Should the gas spent by the INIT method charged too? + Ok(_) => receipt.gas_spent += deploy_charge, + Err(err) => { + let msg = format!("failed deployment: {err:?}"); + receipt.data = Err(ContractError::Panic(msg)) + } + } + } + } + } +} From 955fe66c7dffdd33328a6378818e1c60b9550a21 Mon Sep 17 00:00:00 2001 From: moana Date: Sat, 21 Dec 2024 17:05:03 +0100 Subject: [PATCH 2/6] core: Add deployment methods --- core/Cargo.toml | 2 ++ core/src/abi.rs | 68 +++++++++++++++++++++++++++++++++++++-- core/src/transfer.rs | 19 +++++++++++ core/src/transfer/data.rs | 8 +++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 2c3174149..a70f4910d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -18,6 +18,8 @@ rkyv = { workspace = true, features = ["size_32"] } bytecheck = { workspace = true } rand = { workspace = true } ff = { workspace = true } +blake3 = { workspace = true } +blake2b_simd = { workspace = true } # plonk dependencies dusk-plonk = { workspace = true, features = ["rkyv-impl", "alloc"], optional = true } diff --git a/core/src/abi.rs b/core/src/abi.rs index fd22be42e..aca5e3f98 100644 --- a/core/src/abi.rs +++ b/core/src/abi.rs @@ -14,6 +14,8 @@ pub use piecrust_uplink::{ #[cfg(feature = "abi")] pub use self::host_queries::*; +use blake2b_simd::Params; + /// Enum storing the metadata identifiers. pub enum Metadata {} @@ -44,10 +46,70 @@ impl Query { pub const VERIFY_BLS_MULTISIG: &'static str = "verify_bls_multisig"; } +/// Generate a [`ContractId`] address from: +/// - slice of bytes, +/// - nonce +/// - owner +/// +/// # Panics +/// Panics if [blake2b-hasher] doesn't produce a [`CONTRACT_ID_BYTES`] +/// bytes long hash. +/// +/// [blake2b-hasher]: [`blake2b_simd::Params.finalize`] +pub fn gen_contract_id( + bytes: impl AsRef<[u8]>, + nonce: u64, + owner: impl AsRef<[u8]>, +) -> ContractId { + let mut hasher = Params::new().hash_length(CONTRACT_ID_BYTES).to_state(); + hasher.update(bytes.as_ref()); + hasher.update(&nonce.to_le_bytes()[..]); + hasher.update(owner.as_ref()); + let hash_bytes: [u8; CONTRACT_ID_BYTES] = hasher + .finalize() + .as_bytes() + .try_into() + .expect("the hash result is exactly `CONTRACT_ID_BYTES` long"); + ContractId::from_bytes(hash_bytes) +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use rand::rngs::StdRng; + use rand::{RngCore, SeedableRng}; + + use super::*; + + #[test] + fn test_gen_contract_id() { + let mut rng = StdRng::seed_from_u64(42); + + let mut bytes = vec![0; 1000]; + rng.fill_bytes(&mut bytes); + + let nonce = rng.next_u64(); + + let mut owner = vec![0, 100]; + rng.fill_bytes(&mut owner); + + let contract_id = + gen_contract_id(bytes.as_slice(), nonce, owner.as_slice()); + + assert_eq!( + contract_id.as_bytes(), + [ + 45, 168, 182, 39, 119, 137, 168, 140, 114, 21, 120, 158, 34, + 126, 244, 221, 151, 72, 109, 178, 82, 229, 84, 128, 92, 123, + 135, 74, 23, 224, 119, 133 + ] + ); + } +} + #[cfg(feature = "abi")] pub(crate) mod host_queries { - use alloc::vec::Vec; - #[cfg(feature = "abi-debug")] pub use piecrust_uplink::debug as piecrust_debug; pub use piecrust_uplink::{ @@ -57,6 +119,8 @@ pub(crate) mod host_queries { * spend_and_execute */ }; + use alloc::vec::Vec; + use dusk_bytes::Serializable; use piecrust_uplink::{host_query, meta_data}; diff --git a/core/src/transfer.rs b/core/src/transfer.rs index 47cedd9a2..4b2d8f891 100644 --- a/core/src/transfer.rs +++ b/core/src/transfer.rs @@ -9,6 +9,7 @@ use alloc::string::String; use alloc::vec::Vec; +use core::cmp::max; use core::fmt::Debug; use bytecheck::CheckBytes; @@ -339,6 +340,24 @@ impl Transaction { Self::Moonlight(tx) => tx.hash(), } } + + /// Returns the charge for a contract deployment. The deployment of a + /// contract will cost at least `min_deploy_points`. + /// If the transaction is not a deploy-transaction, the deploy-charge will + /// be 0. + #[must_use] + pub fn deploy_charge( + &self, + gas_per_deploy_byte: u64, + min_deploy_points: u64, + ) -> u64 { + if let Some(deploy) = self.deploy() { + let bytecode_len = deploy.bytecode.bytes.len() as u64; + max(bytecode_len * gas_per_deploy_byte, min_deploy_points) + } else { + 0 + } + } } impl From for Transaction { diff --git a/core/src/transfer/data.rs b/core/src/transfer/data.rs index a2685c100..b4ebbcaa2 100644 --- a/core/src/transfer/data.rs +++ b/core/src/transfer/data.rs @@ -213,6 +213,14 @@ impl ContractBytecode { self.hash.to_vec() } + /// Verifies that the stored bytes-hash is correct. + #[must_use] + pub fn verify_hash(&self) -> bool { + let computed: [u8; 32] = blake3::hash(self.bytes.as_slice()).into(); + + self.hash == computed + } + /// Serializes this object into a variable length buffer #[must_use] pub fn to_var_bytes(&self) -> Vec { From ed18d6810aa18315ee62135571c797cc1e13f922 Mon Sep 17 00:00:00 2001 From: moana Date: Sat, 21 Dec 2024 17:05:55 +0100 Subject: [PATCH 3/6] transfer-contract: Use `vm::execute` for tests --- contracts/transfer/tests/common/utils.rs | 40 +----------- contracts/transfer/tests/moonlight.rs | 83 ++++++++++++------------ contracts/transfer/tests/phoenix.rs | 42 ++++++------ 3 files changed, 68 insertions(+), 97 deletions(-) diff --git a/contracts/transfer/tests/common/utils.rs b/contracts/transfer/tests/common/utils.rs index c42bfd22b..82f26c52e 100644 --- a/contracts/transfer/tests/common/utils.rs +++ b/contracts/transfer/tests/common/utils.rs @@ -6,13 +6,13 @@ use std::sync::mpsc; -use dusk_core::abi::{ContractError, ContractId}; +use dusk_core::abi::ContractId; use dusk_core::signatures::bls::PublicKey as AccountPublicKey; use dusk_core::transfer::moonlight::AccountData; use dusk_core::transfer::phoenix::{Note, NoteLeaf, ViewKey as PhoenixViewKey}; -use dusk_core::transfer::{Transaction, TRANSFER_CONTRACT}; +use dusk_core::transfer::TRANSFER_CONTRACT; use dusk_core::BlsScalar; -use dusk_vm::{CallReceipt, Error as VMError, Session}; +use dusk_vm::{Error as VMError, Session}; const GAS_LIMIT: u64 = 0x10_000_000; @@ -31,40 +31,6 @@ pub fn chain_id(session: &mut Session) -> Result { .map(|r| r.data) } -/// Executes a transaction. -/// Returns result containing gas spent. -pub fn execute( - session: &mut Session, - tx: impl Into, -) -> Result, ContractError>>, VMError> { - let tx = tx.into(); - - let mut receipt = session.call::<_, Result, ContractError>>( - TRANSFER_CONTRACT, - "spend_and_execute", - &tx, - tx.gas_limit(), - )?; - - // Ensure all gas is consumed if there's an error in the contract call - if receipt.data.is_err() { - receipt.gas_spent = receipt.gas_limit; - } - - let refund_receipt = session - .call::<_, ()>( - TRANSFER_CONTRACT, - "refund", - &receipt.gas_spent, - u64::MAX, - ) - .expect("Refunding must succeed"); - - receipt.events.extend(refund_receipt.events); - - Ok(receipt) -} - // moonlight helper functions pub fn account( diff --git a/contracts/transfer/tests/moonlight.rs b/contracts/transfer/tests/moonlight.rs index 5f8f0dfc1..a42d80c3b 100644 --- a/contracts/transfer/tests/moonlight.rs +++ b/contracts/transfer/tests/moonlight.rs @@ -4,17 +4,6 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -pub mod common; - -use crate::common::utils::{ - account, chain_id, contract_balance, execute, existing_nullifiers, - filter_notes_owned_by, leaves_from_height, owned_notes_value, update_root, -}; - -use ff::Field; -use rand::rngs::StdRng; -use rand::SeedableRng; - use dusk_core::abi::{ContractError, ContractId}; use dusk_core::signatures::bls::{ PublicKey as AccountPublicKey, SecretKey as AccountSecretKey, @@ -29,10 +18,21 @@ use dusk_core::transfer::withdraw::{ Withdraw, WithdrawReceiver, WithdrawReplayToken, }; use dusk_core::transfer::{ - ContractToAccount, ContractToContract, TRANSFER_CONTRACT, + ContractToAccount, ContractToContract, Transaction, TRANSFER_CONTRACT, }; use dusk_core::{dusk, JubJubScalar, LUX}; -use dusk_vm::{new_genesis_session, new_session, ContractData, Session, VM}; +use dusk_vm::{ + execute, new_genesis_session, new_session, ContractData, Session, VM, +}; +use ff::Field; +use rand::rngs::StdRng; +use rand::SeedableRng; + +pub mod common; +use crate::common::utils::{ + account, chain_id, contract_balance, existing_nullifiers, + filter_notes_owned_by, leaves_from_height, owned_notes_value, update_root, +}; const MOONLIGHT_GENESIS_VALUE: u64 = dusk(1_000.0); const MOONLIGHT_GENESIS_NONCE: u64 = 0; @@ -171,7 +171,7 @@ fn transfer() { let session = &mut instantiate(&moonlight_sender_pk); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sender_sk, Some(moonlight_receiver_pk), TRANSFER_VALUE, @@ -184,7 +184,7 @@ fn transfer() { ) .expect("Creating moonlight transaction should succeed"); - let gas_spent = execute(session, transaction) + let gas_spent = execute(session, &transaction, 0, 0, 0) .expect("Transaction should succeed") .gas_spent; @@ -233,7 +233,7 @@ fn transfer_with_refund() { "The receiver account should be empty" ); - let transaction = MoonlightTransaction::new_with_refund( + let transaction: Transaction = MoonlightTransaction::new_with_refund( &moonlight_sender_sk, &moonlight_refund_pk, Some(moonlight_receiver_pk), @@ -245,10 +245,11 @@ fn transfer_with_refund() { CHAIN_ID, None::, ) - .expect("Creating moonlight transaction should succeed"); + .expect("Creating moonlight transaction should succeed") + .into(); let max_gas = GAS_LIMIT * LUX; - let gas_spent = execute(session, transaction) + let gas_spent = execute(session, &transaction, 0, 0, 0) .expect("Transaction should succeed") .gas_spent; let gas_refund = max_gas - gas_spent; @@ -300,7 +301,7 @@ fn transfer_gas_fails() { "The receiver account should be empty" ); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sender_sk, Some(moonlight_receiver_pk), TRANSFER_VALUE, @@ -313,7 +314,7 @@ fn transfer_gas_fails() { ) .expect("Creating moonlight transaction should succeed"); - let result = execute(session, transaction); + let result = execute(session, &transaction, 0, 0, 0); assert!( result.is_err(), @@ -352,7 +353,7 @@ fn alice_ping() { fn_args: vec![], }); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sk, None, 0, @@ -365,7 +366,7 @@ fn alice_ping() { ) .expect("Creating moonlight transaction should succeed"); - let gas_spent = execute(session, transaction) + let gas_spent = execute(session, &transaction, 0, 0, 0) .expect("Transaction should succeed") .gas_spent; @@ -432,7 +433,7 @@ fn convert_to_phoenix() { .to_vec(), }; - let tx = MoonlightTransaction::new( + let tx = Transaction::moonlight( &moonlight_sk, None, 0, @@ -446,7 +447,7 @@ fn convert_to_phoenix() { ) .expect("Creating moonlight transaction should succeed"); - let gas_spent = execute(&mut session, tx) + let gas_spent = execute(&mut session, &tx, 0, 0, 0) .expect("Executing transaction should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -552,7 +553,7 @@ fn convert_to_moonlight_fails() { .to_vec(), }; - let tx = MoonlightTransaction::new( + let tx = Transaction::moonlight( &moonlight_sk, None, 0, @@ -566,8 +567,8 @@ fn convert_to_moonlight_fails() { ) .expect("Creating moonlight transaction should succeed"); - let receipt = - execute(&mut session, tx).expect("Executing TX should succeed"); + let receipt = execute(&mut session, &tx, 0, 0, 0) + .expect("Executing TX should succeed"); // check that the transaction execution panicked with the correct message assert!(receipt.data.is_err()); @@ -659,7 +660,7 @@ fn convert_wrong_contract_targeted() { .to_vec(), }; - let tx = MoonlightTransaction::new( + let tx = Transaction::moonlight( &moonlight_sk, None, 0, @@ -672,7 +673,7 @@ fn convert_wrong_contract_targeted() { ) .expect("Creating moonlight transaction should succeed"); - let receipt = execute(&mut session, tx) + let receipt = execute(&mut session, &tx, 0, 0, 0) .expect("Executing transaction should succeed"); update_root(session).expect("Updating the root should succeed"); @@ -731,7 +732,7 @@ fn contract_to_contract() { .to_vec(), }); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sk, None, 0, @@ -744,8 +745,8 @@ fn contract_to_contract() { ) .expect("Creating moonlight transaction should succeed"); - let receipt = - execute(session, transaction).expect("Transaction should succeed"); + let receipt = execute(session, &transaction, 0, 0, 0) + .expect("Transaction should succeed"); let gas_spent = receipt.gas_spent; println!("SEND TO CONTRACT: {:?}", receipt.data); @@ -798,7 +799,7 @@ fn contract_to_account() { .to_vec(), }); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sk, None, 0, @@ -811,8 +812,8 @@ fn contract_to_account() { ) .expect("Creating moonlight transaction should succeed"); - let receipt = - execute(session, transaction).expect("Transaction should succeed"); + let receipt = execute(session, &transaction, 0, 0, 0) + .expect("Transaction should succeed"); let gas_spent = receipt.gas_spent; println!("SEND TO ACCOUNT: {:?}", receipt.data); @@ -862,7 +863,7 @@ fn contract_to_account_insufficient_funds() { .to_vec(), }); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sk, None, 0, @@ -875,8 +876,8 @@ fn contract_to_account_insufficient_funds() { ) .expect("Creating moonlight transaction should succeed"); - let receipt = - execute(session, transaction).expect("Transaction should succeed"); + let receipt = execute(session, &transaction, 0, 0, 0) + .expect("Transaction should succeed"); let gas_spent = receipt.gas_spent; println!("SEND TO ACCOUNT (insufficient funds): {:?}", receipt.data); @@ -933,7 +934,7 @@ fn contract_to_account_direct_call() { .to_vec(), }); - let transaction = MoonlightTransaction::new( + let transaction = Transaction::moonlight( &moonlight_sk, None, 0, @@ -946,8 +947,8 @@ fn contract_to_account_direct_call() { ) .expect("Creating moonlight transaction should succeed"); - let receipt = - execute(session, transaction).expect("Transaction should succeed"); + let receipt = execute(session, &transaction, 0, 0, 0) + .expect("Transaction should succeed"); let gas_spent = receipt.gas_spent; println!( diff --git a/contracts/transfer/tests/phoenix.rs b/contracts/transfer/tests/phoenix.rs index 97496751c..0b3bbc0fb 100644 --- a/contracts/transfer/tests/phoenix.rs +++ b/contracts/transfer/tests/phoenix.rs @@ -24,12 +24,12 @@ use dusk_core::transfer::withdraw::{ Withdraw, WithdrawReceiver, WithdrawReplayToken, }; use dusk_core::transfer::{ - ContractToAccount, ContractToContract, TRANSFER_CONTRACT, + ContractToAccount, ContractToContract, Transaction, TRANSFER_CONTRACT, }; use dusk_core::{BlsScalar, JubJubScalar, LUX}; use dusk_vm::{ - new_genesis_session, new_session, ContractData, Error as VMError, Session, - VM, + execute, new_genesis_session, new_session, ContractData, Error as VMError, + Session, VM, }; use ff::Field; use rand::rngs::StdRng; @@ -37,7 +37,7 @@ use rand::{CryptoRng, RngCore, SeedableRng}; use rusk_prover::LocalProver; use crate::common::utils::{ - account, chain_id, contract_balance, execute, existing_nullifiers, + account, chain_id, contract_balance, existing_nullifiers, filter_notes_owned_by, leaves_from_height, new_owned_notes_value, owned_notes_value, update_root, }; @@ -254,7 +254,7 @@ fn transfer_1_2() { contract_call, ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -361,7 +361,7 @@ fn transfer_2_2() { contract_call, ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -469,7 +469,7 @@ fn transfer_3_2() { contract_call, ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -577,7 +577,7 @@ fn transfer_4_2() { contract_call, ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -681,7 +681,7 @@ fn transfer_gas_fails() { let total_num_notes_before_tx = num_notes(session).expect("Getting num_notes should succeed"); - let result = execute(session, tx); + let result = execute(session, &tx, 0, 0, 0); assert!( result.is_err(), @@ -752,7 +752,7 @@ fn alice_ping() { contract_call, ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -822,7 +822,7 @@ fn contract_deposit() { contract_call, ); - let gas_spent = execute(session, tx.clone()) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -835,7 +835,7 @@ fn contract_deposit() { PHOENIX_GENESIS_VALUE, transfer_value + tx.deposit() - + tx.max_fee() + + tx.gas_limit() * tx.gas_price() + tx.outputs()[1] .value(Some(&PhoenixViewKey::from(&phoenix_sender_sk))) .unwrap() @@ -930,7 +930,7 @@ fn contract_withdraw() { contract_call, ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -1056,7 +1056,8 @@ fn convert_to_phoenix_fails() { Some(contract_call), ); - let receipt = execute(session, tx).expect("Executing TX should succeed"); + let receipt = + execute(session, &tx, 0, 0, 0).expect("Executing TX should succeed"); // check that the transaction execution panicked with the correct message assert!(receipt.data.is_err()); @@ -1176,7 +1177,7 @@ fn convert_to_moonlight() { Some(contract_call), ); - let gas_spent = execute(session, tx) + let gas_spent = execute(session, &tx, 0, 0, 0) .expect("Executing TX should succeed") .gas_spent; update_root(session).expect("Updating the root should succeed"); @@ -1286,7 +1287,7 @@ fn convert_wrong_contract_targeted() { Some(contract_call), ); - let receipt = execute(&mut session, tx) + let receipt = execute(&mut session, &tx, 0, 0, 0) .expect("Executing transaction should succeed"); update_root(session).expect("Updating the root should succeed"); @@ -1377,7 +1378,8 @@ fn contract_to_contract() { Some(contract_call), ); - let receipt = execute(session, tx).expect("Transaction should succeed"); + let receipt = + execute(session, &tx, 0, 0, 0).expect("Transaction should succeed"); let gas_spent = receipt.gas_spent; println!("CONTRACT TO CONTRACT: {gas_spent} gas"); @@ -1470,7 +1472,8 @@ fn contract_to_account() { Some(contract_call), ); - let receipt = execute(session, tx).expect("Transaction should succeed"); + let receipt = + execute(session, &tx, 0, 0, 0).expect("Transaction should succeed"); let gas_spent = receipt.gas_spent; println!("CONTRACT TO ACCOUNT: {gas_spent} gas"); @@ -1585,7 +1588,7 @@ fn create_phoenix_transaction( obfuscated_transaction: bool, deposit: u64, data: Option>, -) -> PhoenixTransaction { +) -> Transaction { // Get the root of the tree of phoenix-notes. let root = root(session).expect("Getting the anchor should be successful"); @@ -1629,4 +1632,5 @@ fn create_phoenix_transaction( &LocalProver, ) .expect("creating the creation shouldn't fail") + .into() } From 28bd7b28114d6faaed081d6b9f6adcbb1bea6a1f Mon Sep 17 00:00:00 2001 From: moana Date: Sat, 21 Dec 2024 17:06:28 +0100 Subject: [PATCH 4/6] stake-contract: Use `vm::execute` for tests --- contracts/stake/tests/common/utils.rs | 41 +------------------------- contracts/stake/tests/partial_stake.rs | 29 +++++++++--------- contracts/stake/tests/stake.rs | 17 +++++------ 3 files changed, 23 insertions(+), 64 deletions(-) diff --git a/contracts/stake/tests/common/utils.rs b/contracts/stake/tests/common/utils.rs index ac4b672e3..8ba795690 100644 --- a/contracts/stake/tests/common/utils.rs +++ b/contracts/stake/tests/common/utils.rs @@ -6,7 +6,6 @@ use std::sync::mpsc; -use dusk_core::abi::ContractError; use dusk_core::transfer::data::TransactionData; use dusk_core::transfer::phoenix::{ Note, NoteLeaf, NoteOpening, NoteTreeItem, PublicKey as PhoenixPublicKey, @@ -15,7 +14,7 @@ use dusk_core::transfer::phoenix::{ }; use dusk_core::transfer::{Transaction, TRANSFER_CONTRACT}; use dusk_core::{BlsScalar, LUX}; -use dusk_vm::{CallReceipt, Error as VMError, Session}; +use dusk_vm::{Error as VMError, Session}; use rand::rngs::StdRng; use rusk_prover::LocalProver; @@ -98,44 +97,6 @@ pub fn filter_notes_owned_by>( .collect() } -/// Executes a transaction, returning the call receipt -pub fn execute( - session: &mut Session, - tx: impl Into, -) -> Result, ContractError>>, VMError> { - let tx = tx.into(); - - // Spend the inputs and execute the call. If this errors the transaction is - // unspendable. - let mut receipt = session.call::<_, Result, ContractError>>( - TRANSFER_CONTRACT, - "spend_and_execute", - &tx, - tx.gas_limit(), - )?; - - // Ensure all gas is consumed if there's an error in the contract call - if receipt.data.is_err() { - receipt.gas_spent = receipt.gas_limit; - } - - // Refund the appropriate amount to the transaction. This call is guaranteed - // to never error. If it does, then a programming error has occurred. As - // such, the call to `Result::expect` is warranted. - let refund_receipt = session - .call::<_, ()>( - TRANSFER_CONTRACT, - "refund", - &receipt.gas_spent, - u64::MAX, - ) - .expect("Refunding must succeed"); - - receipt.events.extend(refund_receipt.events); - - Ok(receipt) -} - /// Generate a TxCircuit given the sender secret-key, receiver public-key, the /// input note positions in the transaction tree and the new output-notes. pub fn create_transaction( diff --git a/contracts/stake/tests/partial_stake.rs b/contracts/stake/tests/partial_stake.rs index 0db5c20ef..e4beb8d4a 100644 --- a/contracts/stake/tests/partial_stake.rs +++ b/contracts/stake/tests/partial_stake.rs @@ -11,8 +11,8 @@ use dusk_core::signatures::bls::{ use dusk_core::stake::{Reward, RewardReason, EPOCH, STAKE_CONTRACT}; use dusk_core::transfer::TRANSFER_CONTRACT; use dusk_vm::{ - new_genesis_session, new_session, ContractData, Error as VMError, Session, - VM, + execute, new_genesis_session, new_session, ContractData, Error as VMError, + Session, VM, }; use rand::rngs::StdRng; use rand::SeedableRng; @@ -21,7 +21,6 @@ use wallet_core::transaction::{ }; pub mod common; - use crate::common::assert::*; use crate::common::init::CHAIN_ID; use crate::common::utils::*; @@ -63,7 +62,7 @@ fn stake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 1st stake transaction let gas_spent_1 = receipt.gas_spent; @@ -91,7 +90,7 @@ fn stake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 2nd stake transaction let gas_spent_2 = receipt.gas_spent; @@ -125,7 +124,7 @@ fn stake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 3rd stake transaction let gas_spent_3 = receipt.gas_spent; @@ -160,7 +159,7 @@ fn stake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - assert!(execute(&mut session, tx,).is_err()); + assert!(execute(&mut session, &tx, 0, 0, 0).is_err()); Ok(()) } @@ -194,7 +193,7 @@ fn unstake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; let mut moonlight_balance = GENESIS_VALUE - STAKE_VALUE - receipt.gas_spent; assert_moonlight(&mut session, &moonlight_pk, moonlight_balance, nonce); @@ -216,7 +215,7 @@ fn unstake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 1st unstake transaction let gas_spent_1 = receipt.gas_spent; @@ -246,7 +245,7 @@ fn unstake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; total_stake = STAKE_VALUE; let mut locked = unstake_1 / 10; assert_stake(&mut session, &stake_pk, total_stake, locked, 0); @@ -272,7 +271,7 @@ fn unstake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 2nd unstake transaction let gas_spent_2 = receipt.gas_spent; @@ -310,7 +309,7 @@ fn unstake() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 3rd unstake transaction let gas_spent_3 = receipt.gas_spent; @@ -353,7 +352,7 @@ fn withdraw_reward() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; let mut moonlight_balance = GENESIS_VALUE - STAKE_VALUE - receipt.gas_spent; assert_moonlight(&mut session, &moonlight_pk, moonlight_balance, nonce); // add a reward to the staked key @@ -378,7 +377,7 @@ fn withdraw_reward() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 1st reward withdrawal let gas_spent_1 = receipt.gas_spent; @@ -413,7 +412,7 @@ fn withdraw_reward() -> Result<(), VMError> { CHAIN_ID, ) .expect("tx creation should pass"); - let receipt = execute(&mut session, tx)?; + let receipt = execute(&mut session, &tx, 0, 0, 0)?; // verify 1st reward withdrawal let gas_spent_2 = receipt.gas_spent; diff --git a/contracts/stake/tests/stake.rs b/contracts/stake/tests/stake.rs index aed9e0f1c..708892076 100644 --- a/contracts/stake/tests/stake.rs +++ b/contracts/stake/tests/stake.rs @@ -4,8 +4,6 @@ // // Copyright (c) DUSK NETWORK. All rights reserved. -pub mod common; - use dusk_core::signatures::bls::{ PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, }; @@ -21,11 +19,12 @@ use dusk_core::transfer::withdraw::{ Withdraw, WithdrawReceiver, WithdrawReplayToken, }; use dusk_core::{dusk, JubJubScalar}; -use dusk_vm::{new_session, VM}; +use dusk_vm::{execute, new_session, VM}; use ff::Field; use rand::rngs::StdRng; use rand::SeedableRng; +pub mod common; use crate::common::assert::{ assert_reward_event, assert_stake, assert_stake_event, }; @@ -86,8 +85,8 @@ fn stake_withdraw_unstake() { contract_call, ); - let receipt = - execute(&mut session, tx).expect("Executing TX should succeed"); + let receipt = execute(&mut session, &tx, 0, 0, 0) + .expect("Executing TX should succeed"); let gas_spent = receipt.gas_spent; receipt.data.expect("Executed TX should not error"); @@ -181,8 +180,8 @@ fn stake_withdraw_unstake() { let mut session = new_session(vm, base, CHAIN_ID, 2) .expect("Instantiating new session should succeed"); - let receipt = - execute(&mut session, tx).expect("Executing TX should succeed"); + let receipt = execute(&mut session, &tx, 0, 0, 0) + .expect("Executing TX should succeed"); let gas_spent = receipt.gas_spent; receipt.data.expect("Executed TX should not error"); @@ -277,8 +276,8 @@ fn stake_withdraw_unstake() { let mut session = new_session(vm, base, CHAIN_ID, 3) .expect("Instantiating new session should succeed"); - let receipt = - execute(&mut session, tx).expect("Executing TX should succeed"); + let receipt = execute(&mut session, &tx, 0, 0, 0) + .expect("Executing TX should succeed"); update_root(&mut session).expect("Updating the root should succeed"); let gas_spent = receipt.gas_spent; From c6b05e6e7b3afa61db370977184810c0547acc13 Mon Sep 17 00:00:00 2001 From: moana Date: Sat, 21 Dec 2024 17:06:53 +0100 Subject: [PATCH 5/6] node: Move `bytecode_charge` function to `core` --- node/src/mempool.rs | 14 ++++++-------- node/src/vm.rs | 14 -------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/node/src/mempool.rs b/node/src/mempool.rs index 2d0470399..a1ec8593d 100644 --- a/node/src/mempool.rs +++ b/node/src/mempool.rs @@ -212,7 +212,7 @@ impl MempoolSrv { return Err(TxAcceptanceError::GasPriceTooLow(1)); } - if let Some(deploy) = tx.inner.deploy() { + if tx.inner.deploy().is_some() { let vm = vm.read().await; let min_deployment_gas_price = vm.min_deployment_gas_price(); if tx.gas_price() < min_deployment_gas_price { @@ -222,13 +222,11 @@ impl MempoolSrv { } let gas_per_deploy_byte = vm.gas_per_deploy_byte(); - let min_gas_limit = vm::bytecode_charge( - &deploy.bytecode, - gas_per_deploy_byte, - vm.min_deploy_points(), - ); - if tx.inner.gas_limit() < min_gas_limit { - return Err(TxAcceptanceError::GasLimitTooLow(min_gas_limit)); + let deploy_charge = tx + .inner + .deploy_charge(gas_per_deploy_byte, vm.min_deploy_points()); + if tx.inner.gas_limit() < deploy_charge { + return Err(TxAcceptanceError::GasLimitTooLow(deploy_charge)); } } else { let vm = vm.read().await; diff --git a/node/src/vm.rs b/node/src/vm.rs index dec91117e..bc104b0a4 100644 --- a/node/src/vm.rs +++ b/node/src/vm.rs @@ -9,11 +9,9 @@ use dusk_consensus::operations::{CallParams, VerificationOutput, Voter}; use dusk_consensus::user::provisioners::Provisioners; use dusk_consensus::user::stake::Stake; use dusk_core::signatures::bls::PublicKey as BlsPublicKey; -use dusk_core::transfer::data::ContractBytecode; use dusk_core::transfer::moonlight::AccountData; use node_data::events::contract::ContractEvent; use node_data::ledger::{Block, SpentTransaction, Transaction}; -use std::cmp::max; #[derive(Default)] pub struct Config {} @@ -102,15 +100,3 @@ pub enum PreverificationResult { nonce_used: u64, }, } - -// Returns gas charge for bytecode deployment. -pub fn bytecode_charge( - bytecode: &ContractBytecode, - gas_per_deploy_byte: u64, - min_deploy_points: u64, -) -> u64 { - max( - bytecode.bytes.len() as u64 * gas_per_deploy_byte, - min_deploy_points, - ) -} From e59474dfdba9fa63007c2c24465fb27e8376f703 Mon Sep 17 00:00:00 2001 From: moana Date: Sat, 21 Dec 2024 17:07:45 +0100 Subject: [PATCH 6/6] rusk: Expose transaction execution function through `dusk-vm` --- rusk/src/lib/gen_id.rs | 57 ------- rusk/src/lib/lib.rs | 1 - rusk/src/lib/node/rusk.rs | 169 +-------------------- rusk/tests/services/contract_deployment.rs | 3 +- rusk/tests/services/contract_stake.rs | 3 +- rusk/tests/services/owner_calls.rs | 3 +- 6 files changed, 8 insertions(+), 228 deletions(-) delete mode 100644 rusk/src/lib/gen_id.rs diff --git a/rusk/src/lib/gen_id.rs b/rusk/src/lib/gen_id.rs deleted file mode 100644 index f93a0963c..000000000 --- a/rusk/src/lib/gen_id.rs +++ /dev/null @@ -1,57 +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 blake2b_simd::Params; -use dusk_core::abi::{ContractId, CONTRACT_ID_BYTES}; - -/// Generate a [`ContractId`] address from: -/// - slice of bytes, -/// - nonce -/// - owner -pub fn gen_contract_id( - bytes: impl AsRef<[u8]>, - nonce: u64, - owner: impl AsRef<[u8]>, -) -> ContractId { - let mut hasher = Params::new().hash_length(CONTRACT_ID_BYTES).to_state(); - hasher.update(bytes.as_ref()); - hasher.update(&nonce.to_le_bytes()[..]); - hasher.update(owner.as_ref()); - let hash_bytes: [u8; CONTRACT_ID_BYTES] = hasher - .finalize() - .as_bytes() - .try_into() - .expect("the hash result is exactly `CONTRACT_ID_BYTES` long"); - ContractId::from_bytes(hash_bytes) -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::rngs::StdRng; - use rand::{RngCore, SeedableRng}; - - #[test] - fn test_gen_contract_id() { - let mut rng = StdRng::seed_from_u64(42); - - let mut bytes = vec![0; 1000]; - rng.fill_bytes(&mut bytes); - - let nonce = rng.next_u64(); - - let mut owner = vec![0, 100]; - rng.fill_bytes(&mut owner); - - let contract_id = - gen_contract_id(bytes.as_slice(), nonce, owner.as_slice()); - - assert_eq!( - hex::encode(contract_id.as_bytes()), - "2da8b6277789a88c7215789e227ef4dd97486db252e554805c7b874a17e07785" - ); - } -} diff --git a/rusk/src/lib/lib.rs b/rusk/src/lib/lib.rs index 541c42bc2..603cef789 100644 --- a/rusk/src/lib/lib.rs +++ b/rusk/src/lib/lib.rs @@ -8,7 +8,6 @@ mod bloom; mod error; -pub mod gen_id; pub mod http; #[cfg(feature = "chain")] pub mod node; diff --git a/rusk/src/lib/node/rusk.rs b/rusk/src/lib/node/rusk.rs index 7d39c289c..df707b2fc 100644 --- a/rusk/src/lib/node/rusk.rs +++ b/rusk/src/lib/node/rusk.rs @@ -16,19 +16,18 @@ use dusk_consensus::config::{ RATIFICATION_COMMITTEE_CREDITS, VALIDATION_COMMITTEE_CREDITS, }; use dusk_consensus::operations::{CallParams, VerificationOutput, Voter}; -use dusk_core::abi::{ContractError, Event}; +use dusk_core::abi::Event; use dusk_core::signatures::bls::PublicKey as BlsPublicKey; use dusk_core::stake::{ Reward, RewardReason, StakeData, StakeKeys, STAKE_CONTRACT, }; use dusk_core::transfer::{ - data::ContractDeploy, moonlight::AccountData, - Transaction as ProtocolTransaction, PANIC_NONCE_NOT_READY, - TRANSFER_CONTRACT, + moonlight::AccountData, PANIC_NONCE_NOT_READY, TRANSFER_CONTRACT, }; use dusk_core::{BlsScalar, Dusk}; -use dusk_vm::{new_session, CallReceipt, Error as VMError, Session, VM}; -use node::vm::bytecode_charge; +use dusk_vm::{ + execute, new_session, CallReceipt, Error as VMError, Session, VM, +}; use node::DUSK_CONSENSUS_KEY; use node_data::events::contract::{ContractEvent, ContractTxEvent}; use node_data::ledger::{Hash, Slash, SpentTransaction, Transaction}; @@ -40,7 +39,6 @@ use tracing::info; use {node_data::archive::ArchivalData, tokio::sync::mpsc::Sender}; use crate::bloom::Bloom; -use crate::gen_id::gen_contract_id; use crate::http::RuesEvent; use crate::node::{coinbase_value, Rusk, RuskTip}; use crate::Error::InvalidCreditsCount; @@ -665,163 +663,6 @@ fn accept( )) } -// Contract deployment will fail and charge full gas limit in the -// following cases: -// 1) Transaction gas limit is smaller than deploy charge plus gas used for -// spending funds. -// 2) Transaction's bytecode's bytes are not consistent with bytecode's hash. -// 3) Deployment fails for deploy-specific reasons like e.g.: -// - contract already deployed -// - corrupted bytecode -// - sufficient gas to spend funds yet insufficient for deployment -fn contract_deploy( - session: &mut Session, - deploy: &ContractDeploy, - gas_limit: u64, - gas_per_deploy_byte: u64, - min_deploy_points: u64, - receipt: &mut CallReceipt, ContractError>>, -) { - let deploy_charge = bytecode_charge( - &deploy.bytecode, - gas_per_deploy_byte, - min_deploy_points, - ); - let min_gas_limit = receipt.gas_spent + deploy_charge; - let hash = blake3::hash(deploy.bytecode.bytes.as_slice()); - if gas_limit < min_gas_limit { - receipt.data = Err(ContractError::OutOfGas); - } else if hash != deploy.bytecode.hash { - receipt.data = - Err(ContractError::Panic("failed bytecode hash check".into())) - } else { - let result = session.deploy_raw( - Some(gen_contract_id( - &deploy.bytecode.bytes, - deploy.nonce, - &deploy.owner, - )), - deploy.bytecode.bytes.as_slice(), - deploy.init_args.clone(), - deploy.owner.clone(), - gas_limit, - ); - match result { - // Should the gas spent by the INIT method charged too? - Ok(_) => receipt.gas_spent += deploy_charge, - Err(err) => { - info!("Tx caused deployment error {err:?}"); - let msg = format!("failed deployment: {err:?}"); - receipt.data = Err(ContractError::Panic(msg)) - } - } - } -} - -/// Executes a transaction, returning the receipt of the call and the gas spent. -/// The following steps are performed: -/// -/// 1. Check if the transaction contains contract deployment data, and if so, -/// verifies if gas limit is enough for deployment and if the gas price is -/// sufficient for deployment. If either gas price or gas limit is not -/// sufficient for deployment, transaction is discarded. -/// -/// 2. Call the "spend_and_execute" function on the transfer contract with -/// unlimited gas. If this fails, an error is returned. If an error is -/// returned the transaction should be considered unspendable/invalid, but no -/// re-execution of previous transactions is required. -/// -/// 3. If the transaction contains contract deployment data, additional checks -/// are performed and if they pass, deployment is executed. The following -/// checks are performed: -/// - gas limit should be is smaller than deploy charge plus gas used for -/// spending funds -/// - transaction's bytecode's bytes are consistent with bytecode's hash -/// Deployment execution may fail for deployment-specific reasons, such as -/// for example: -/// - contract already deployed -/// - corrupted bytecode -/// If deployment execution fails, the entire gas limit is consumed and error -/// is returned. -/// -/// 4. Call the "refund" function on the transfer contract with unlimited gas. -/// The amount charged depends on the gas spent by the transaction, and the -/// optional contract call in steps 2 or 3. -/// -/// Note that deployment transaction will never be re-executed for reasons -/// related to deployment, as it is either discarded or it charges the -/// full gas limit. It might be re-executed only if some other transaction -/// failed to fit the block. -fn execute( - session: &mut Session, - tx: &ProtocolTransaction, - gas_per_deploy_byte: u64, - min_deploy_points: u64, - min_deployment_gas_price: u64, -) -> Result, ContractError>>, VMError> { - // Transaction will be discarded if it is a deployment transaction - // with gas limit smaller than deploy charge. - if let Some(deploy) = tx.deploy() { - let deploy_charge = bytecode_charge( - &deploy.bytecode, - gas_per_deploy_byte, - min_deploy_points, - ); - if tx.gas_price() < min_deployment_gas_price { - return Err(VMError::Panic("gas price too low to deploy".into())); - } - if tx.gas_limit() < deploy_charge { - return Err(VMError::Panic("not enough gas to deploy".into())); - } - } - - let tx_stripped = tx.strip_off_bytecode(); - // Spend the inputs and execute the call. If this errors the transaction is - // unspendable. - let mut receipt = session.call::<_, Result, ContractError>>( - TRANSFER_CONTRACT, - "spend_and_execute", - tx_stripped.as_ref().unwrap_or(tx), - tx.gas_limit(), - )?; - - // Deploy if this is a deployment transaction and spend part is successful. - if let Some(deploy) = tx.deploy() { - let gas_left = tx.gas_limit() - receipt.gas_spent; - if receipt.data.is_ok() { - contract_deploy( - session, - deploy, - gas_left, - gas_per_deploy_byte, - min_deploy_points, - &mut receipt, - ); - } - }; - - // Ensure all gas is consumed if there's an error in the contract call - if receipt.data.is_err() { - receipt.gas_spent = receipt.gas_limit; - } - - // Refund the appropriate amount to the transaction. This call is guaranteed - // to never error. If it does, then a programming error has occurred. As - // such, the call to `Result::expect` is warranted. - let refund_receipt = session - .call::<_, ()>( - TRANSFER_CONTRACT, - "refund", - &receipt.gas_spent, - u64::MAX, - ) - .expect("Refunding must succeed"); - - receipt.events.extend(refund_receipt.events); - - Ok(receipt) -} - fn reward_slash_and_update_root( session: &mut Session, block_height: u64, diff --git a/rusk/tests/services/contract_deployment.rs b/rusk/tests/services/contract_deployment.rs index 83c0fdd09..4225ed43f 100644 --- a/rusk/tests/services/contract_deployment.rs +++ b/rusk/tests/services/contract_deployment.rs @@ -8,14 +8,13 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; -use dusk_core::abi::ContractId; +use dusk_core::abi::{gen_contract_id, ContractId}; use dusk_core::transfer::data::{ ContractBytecode, ContractDeploy, TransactionData, }; use dusk_vm::{new_session, ContractData, Error as VMError, VM}; use rand::prelude::*; use rand::rngs::StdRng; -use rusk::gen_id::gen_contract_id; use rusk::{Result, Rusk}; use rusk_recovery_tools::state; use tempfile::tempdir; diff --git a/rusk/tests/services/contract_stake.rs b/rusk/tests/services/contract_stake.rs index 83a853f4b..e9b37fcdd 100644 --- a/rusk/tests/services/contract_stake.rs +++ b/rusk/tests/services/contract_stake.rs @@ -10,13 +10,12 @@ use std::sync::{Arc, RwLock}; use dusk_core::stake::{self, Stake, DEFAULT_MINIMUM_STAKE, EPOCH}; use dusk_bytes::Serializable; -use dusk_core::abi::ContractId; +use dusk_core::abi::{gen_contract_id, ContractId}; use dusk_core::transfer::data::ContractCall; use dusk_core::transfer::{self, Transaction}; use node_data::ledger::SpentTransaction; use rand::prelude::*; use rand::rngs::StdRng; -use rusk::gen_id::gen_contract_id; use rusk::{Result, Rusk}; use std::collections::HashMap; use tempfile::tempdir; diff --git a/rusk/tests/services/owner_calls.rs b/rusk/tests/services/owner_calls.rs index 451614150..8151c590c 100644 --- a/rusk/tests/services/owner_calls.rs +++ b/rusk/tests/services/owner_calls.rs @@ -14,13 +14,12 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::sync::{Arc, RwLock}; -use dusk_core::abi::ContractId; +use dusk_core::abi::{gen_contract_id, ContractId}; use dusk_core::signatures::bls::{ PublicKey as BlsPublicKey, SecretKey as BlsSecretKey, Signature as BlsSignature, }; use dusk_vm::{new_session, CallReceipt, ContractData, Session, VM}; -use rusk::gen_id::gen_contract_id; use rusk::{Error, Result, Rusk}; use rusk_recovery_tools::state; use tempfile::tempdir;