Repository: dusk-network/rusk (language: rust)

Issue:
vm: Pick up review comments
#### Summary
Pick up the review comments from #3237 and #3185
- [x] make `session::new` and `session::genesis` methods under the `VM` struct (possibly hide the current `VM::session`
- [x] fix `RUSK_ABI_` prefix for cache size environment variable in `dusk_vm::cache`
- [x] remove 'vm/rustfmt.toml' file
- [x] move `gen_contract_id` from `dusk-core` to `dusk_vm`
- [x] move contract bytecode hash verification to `dusk-vm` to minimize deps of `dusk-core`


Discussion / hints:

Fix patch:
diff --git a/contracts/stake/benches/get_provisioners.rs b/contracts/stake/benches/get_provisioners.rs
index cb4d8a9933..164952c5c3 100644
--- a/contracts/stake/benches/get_provisioners.rs
+++ b/contracts/stake/benches/get_provisioners.rs
@@ -22,6 +22,7 @@ const NUM_STAKES: usize = 1000;
 const OWNER: [u8; 32] = [0; 32];
 const POINT_LIMIT: u64 = 0x100000000;
 const TEST_STAKE: u64 = 500_000_000_000_000;
+const CHAIN_ID: u8 = 42;
 
 fn config() -> Criterion {
     Criterion::default().sample_size(SAMPLE_SIZE)
@@ -41,7 +42,7 @@ fn instantiate(vm: &VM) -> Session {
         "../../../target/dusk/wasm32-unknown-unknown/release/stake_contract.wasm"
     );
 
-    let mut session = abi::new_genesis_session(vm);
+    let mut session = vm.genesis_session(CHAIN_ID);
 
     session
         .deploy(
@@ -63,7 +64,7 @@ fn instantiate(vm: &VM) -> Session {
 
     let base = session.commit().expect("Committing should succeed");
 
-    abi::new_session(vm, base, 1)
+    vm.session(base, CHAIN_ID, 1)
         .expect("Instantiating new session should succeed")
 }
 
diff --git a/core/src/abi.rs b/core/src/abi.rs
index fd22be42e4..94a5232d4c 100644
--- a/core/src/abi.rs
+++ b/core/src/abi.rs
@@ -46,8 +46,6 @@ impl Query {
 
 #[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 +55,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/error.rs b/core/src/error.rs
index d4e5bfcd65..66a1a0a3cc 100644
--- a/core/src/error.rs
+++ b/core/src/error.rs
@@ -39,7 +39,7 @@ pub enum Error {
 
 impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "Execution-Core Error: {:?}", &self)
+        write!(f, "Dusk-Core Error: {:?}", &self)
     }
 }
 
diff --git a/core/src/transfer.rs b/core/src/transfer.rs
index 47cedd9a2b..4b2d8f8914 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<PhoenixTransaction> for Transaction {
diff --git a/node/src/mempool.rs b/node/src/mempool.rs
index 2d0470399e..a1ec8593d0 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 dec91117e9..bc104b0a4b 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,
-    )
-}
diff --git a/rusk-recovery/src/state.rs b/rusk-recovery/src/state.rs
index ea60b9bef7..71a286557b 100644
--- a/rusk-recovery/src/state.rs
+++ b/rusk-recovery/src/state.rs
@@ -15,7 +15,7 @@ use dusk_core::stake::{StakeAmount, StakeData, StakeKeys, STAKE_CONTRACT};
 use dusk_core::transfer::phoenix::{Note, PublicKey, Sender};
 use dusk_core::transfer::TRANSFER_CONTRACT;
 use dusk_core::JubJubScalar;
-use dusk_vm::{new_genesis_session, new_session, ContractData, Session, VM};
+use dusk_vm::{ContractData, Session, VM};
 use ff::Field;
 use once_cell::sync::Lazy;
 use rand::rngs::StdRng;
@@ -177,7 +177,7 @@ fn generate_empty_state<P: AsRef<Path>>(
     let state_dir = state_dir.as_ref();
 
     let vm = VM::new(state_dir)?;
-    let mut session = new_genesis_session(&vm, GENESIS_CHAIN_ID);
+    let mut session = vm.genesis_session(GENESIS_CHAIN_ID);
 
     let transfer_code = include_bytes!(
         "../../target/dusk/wasm64-unknown-unknown/release/transfer_contract.wasm"
@@ -260,12 +260,8 @@ where
         None => generate_empty_state(state_dir, snapshot),
     }?;
 
-    let mut session = new_session(
-        &vm,
-        old_commit_id,
-        GENESIS_CHAIN_ID,
-        GENESIS_BLOCK_HEIGHT,
-    )?;
+    let mut session =
+        vm.session(old_commit_id, GENESIS_CHAIN_ID, GENESIS_BLOCK_HEIGHT)?;
 
     generate_transfer_state(&mut session, snapshot)?;
     generate_stake_state(&mut session, snapshot)?;
diff --git a/rusk/src/lib/gen_id.rs b/rusk/src/lib/gen_id.rs
deleted file mode 100644
index f93a0963cd..0000000000
--- 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 541c42bc2b..603cef789f 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 7d39c289c2..e0b2ec3043 100644
--- a/rusk/src/lib/node/rusk.rs
+++ b/rusk/src/lib/node/rusk.rs
@@ -16,19 +16,16 @@ 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, 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 +37,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;
@@ -530,8 +526,7 @@ impl Rusk {
             tip.current
         });
 
-        let session =
-            new_session(&self.vm, commit, self.chain_id, block_height)?;
+        let session = self.vm.session(commit, self.chain_id, block_height)?;
 
         Ok(session)
     }
@@ -550,7 +545,7 @@ impl Rusk {
         for d in to_merge {
             if d == base {
                 // Don't finalize the new tip, otherwise it will not be
-                // aceessible anymore
+                // accessible anymore
                 continue;
             };
             self.vm.finalize_commit(d)?;
@@ -665,163 +660,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<Result<Vec<u8>, 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<CallReceipt<Result<Vec<u8>, 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<Vec<u8>, 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/vm/Cargo.toml b/vm/Cargo.toml
index b5fb1ab8fd..cab154ef56 100644
--- a/vm/Cargo.toml
+++ b/vm/Cargo.toml
@@ -14,6 +14,7 @@ dusk-bytes = { workspace = true }
 piecrust = { workspace = true }
 lru = { workspace = true }
 blake2b_simd = { workspace = true }
+blake3 = { workspace = true }
 dusk-poseidon = { workspace = true }
 rkyv = { workspace = true, features = ["size_32"] }
 
diff --git a/vm/rustfmt.toml b/vm/rustfmt.toml
deleted file mode 100644
index 3450fc407a..0000000000
--- a/vm/rustfmt.toml
+++ /dev/null
@@ -1,2 +0,0 @@
-max_width = 80
-wrap_comments = true
diff --git a/vm/src/cache.rs b/vm/src/cache.rs
index 103b73b607..a70ea16335 100644
--- a/vm/src/cache.rs
+++ b/vm/src/cache.rs
@@ -76,7 +76,7 @@ define_cache!(
     with_plonk_cache,
     bool,
     512,
-    "RUSK_ABI_PLONK_CACHE_SIZE"
+    "DUSK_VM_PLONK_CACHE_SIZE"
 );
 define_cache!(
     get_groth16_verification,
@@ -84,7 +84,7 @@ define_cache!(
     with_groth16_cache,
     bool,
     512,
-    "RUSK_ABI_GROTH16_CACHE_SIZE"
+    "DUSK_VM_GROTH16_CACHE_SIZE"
 );
 define_cache!(
     get_bls_verification,
@@ -92,5 +92,5 @@ define_cache!(
     with_bls_cache,
     bool,
     512,
-    "RUSK_ABI_BLS_CACHE_SIZE"
+    "DUSK_VM_BLS_CACHE_SIZE"
 );
diff --git a/vm/src/execute.rs b/vm/src/execute.rs
new file mode 100644
index 0000000000..6d6900e06a
--- /dev/null
+++ b/vm/src/execute.rs
@@ -0,0 +1,243 @@
+// 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::{ContractError, ContractId, CONTRACT_ID_BYTES};
+use dusk_core::transfer::{
+    data::ContractBytecode, Transaction, TRANSFER_CONTRACT,
+};
+use piecrust::{CallReceipt, Error, Session};
+
+/// 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<CallReceipt<Result<Vec<u8>, 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<Vec<u8>, 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<Result<Vec<u8>, 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 !verify_bytecode_hash(&deploy.bytecode) {
+                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))
+                    }
+                }
+            }
+        }
+    }
+}
+
+// Verifies that the stored contract bytecode hash is correct.
+fn verify_bytecode_hash(bytecode: &ContractBytecode) -> bool {
+    let computed: [u8; 32] = blake3::hash(bytecode.bytes.as_slice()).into();
+
+    bytecode.hash == computed
+}
+
+/// 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;
+
+    // the `unused_crate_dependencies` lint complains for dev-dependencies that
+    // are only used in integration tests, so adding this work-around here
+    use ff as _;
+    use once_cell as _;
+    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
+            ]
+        );
+    }
+}
diff --git a/vm/src/lib.rs b/vm/src/lib.rs
index 4de3468896..444c1fb2bc 100644
--- a/vm/src/lib.rs
+++ b/vm/src/lib.rs
@@ -13,6 +13,7 @@
 
 extern crate alloc;
 
+pub use self::execute::{execute, gen_contract_id};
 pub use piecrust::{
     CallReceipt, CallTree, CallTreeElem, ContractData, Error, PageOpening,
     Session,
@@ -32,35 +33,9 @@ use self::host_queries::{
 };
 
 pub(crate) mod cache;
+mod execute;
 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<Session, Error> {
-    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")
-}
-
 /// Dusk VM is a [`PiecrustVM`] enriched with the host functions specified in
 /// Dusk's ABI.
 pub struct VM(PiecrustVM);
@@ -107,7 +82,8 @@ impl VM {
         Ok(vm)
     }
 
-    /// Spawn a [`Session`].
+    /// Spawn a [`Session`] on top of the given `commit`, given the `chain_id`
+    /// and `block_height`.
     ///
     /// # Errors
     /// If base commit is provided but does not exist.
@@ -115,9 +91,32 @@ impl VM {
     /// [`Session`]: Session
     pub fn session(
         &self,
-        data: impl Into<SessionData>,
+        base: [u8; 32],
+        chain_id: u8,
+        block_height: u64,
     ) -> Result<Session, Error> {
-        self.0.session(data)
+        self.0.session(
+            SessionData::builder()
+                .base(base)
+                .insert(Metadata::CHAIN_ID, chain_id)?
+                .insert(Metadata::BLOCK_HEIGHT, block_height)?,
+        )
+    }
+
+    /// Spawn a new genesis-[`Session`] given the `chain_id` with a
+    /// `block_height` of 0.
+    pub fn genesis_session(&self, chain_id: u8) -> Session {
+        self.0
+            .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")
     }
 
     /// Return all existing commits.
@@ -171,12 +170,3 @@ impl VM {
         );
     }
 }
-
-#[cfg(test)]
-mod tests {
-    // the `unused_crate_dependencies` lint complains for dev-dependencies that
-    // are only used in integration tests, so adding this work-around here
-    use ff as _;
-    use once_cell as _;
-    use rand as _;
-}


Test patch:
diff --git a/contracts/stake/tests/common/init.rs b/contracts/stake/tests/common/init.rs
index 3e0ec49d66..e697b8460f 100644
--- a/contracts/stake/tests/common/init.rs
+++ b/contracts/stake/tests/common/init.rs
@@ -27,7 +27,7 @@ pub fn instantiate<Rng: RngCore + CryptoRng>(
     pk: &PhoenixPublicKey,
     genesis_value: u64,
 ) -> Session {
-    let mut session = dusk_vm::new_genesis_session(vm, CHAIN_ID);
+    let mut session = vm.genesis_session(CHAIN_ID);
 
     // deploy transfer-contract
     let transfer_bytecode = include_bytes!(
@@ -81,6 +81,6 @@ pub fn instantiate<Rng: RngCore + CryptoRng>(
     // sets the block height for all subsequent operations to 1
     let base = session.commit().expect("Committing should succeed");
 
-    dusk_vm::new_session(vm, base, CHAIN_ID, 1)
+    vm.session(base, CHAIN_ID, 1)
         .expect("Instantiating new session should succeed")
 }
diff --git a/contracts/stake/tests/common/utils.rs b/contracts/stake/tests/common/utils.rs
index ac4b672e36..8ba7956908 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<I: IntoIterator<Item = Note>>(
         .collect()
 }
 
-/// Executes a transaction, returning the call receipt
-pub fn execute(
-    session: &mut Session,
-    tx: impl Into<Transaction>,
-) -> Result<CallReceipt<Result<Vec<u8>, 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<Vec<u8>, 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<const I: usize>(
diff --git a/contracts/stake/tests/partial_stake.rs b/contracts/stake/tests/partial_stake.rs
index 0db5c20efc..e4759b8679 100644
--- a/contracts/stake/tests/partial_stake.rs
+++ b/contracts/stake/tests/partial_stake.rs
@@ -10,10 +10,7 @@ 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,
-};
+use dusk_vm::{execute, ContractData, Error as VMError, Session, VM};
 use rand::rngs::StdRng;
 use rand::SeedableRng;
 use wallet_core::transaction::{
@@ -21,7 +18,6 @@ use wallet_core::transaction::{
 };
 
 pub mod common;
-
 use crate::common::assert::*;
 use crate::common::init::CHAIN_ID;
 use crate::common::utils::*;
@@ -63,7 +59,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 +87,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;
@@ -109,7 +105,7 @@ fn stake() -> Result<(), VMError> {
     // in order to test the locking some of the stake during a top-up, we need
     // to start a new session at a block-height on which the stake is eligible
     let base = session.commit()?;
-    let mut session = new_session(&vm, base, CHAIN_ID, 2 * EPOCH)?;
+    let mut session = vm.session(base, CHAIN_ID, 2 * EPOCH)?;
 
     // execute 3rd stake transaction
     let stake_3 = STAKE_VALUE - stake_1 - stake_2;
@@ -125,7 +121,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 +156,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 +190,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 +212,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;
@@ -233,7 +229,7 @@ fn unstake() -> Result<(), VMError> {
 
     // re-stake the unstaked value after the stake has become eligible
     let base = session.commit()?;
-    let mut session = new_session(&vm, base, CHAIN_ID, 2 * EPOCH)?;
+    let mut session = vm.session(base, CHAIN_ID, 2 * EPOCH)?;
     nonce += 1;
     let tx = moonlight_stake(
         &moonlight_sk,
@@ -246,7 +242,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 +268,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 +306,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 +349,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 +374,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 +409,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;
@@ -458,7 +454,7 @@ fn add_reward(
 /// genesis-value.
 fn instantiate(vm: &mut VM, moonlight_pk: &BlsPublicKey) -> Session {
     // create a new session using an ephemeral vm
-    let mut session = new_genesis_session(vm, CHAIN_ID);
+    let mut session = vm.genesis_session(CHAIN_ID);
 
     // deploy transfer-contract
     const OWNER: [u8; 32] = [0; 32];
@@ -502,7 +498,8 @@ fn instantiate(vm: &mut VM, moonlight_pk: &BlsPublicKey) -> Session {
     // sets the block height for all subsequent operations to 1
     let base = session.commit().expect("Committing should succeed");
 
-    let mut session = new_session(vm, base, CHAIN_ID, 1)
+    let mut session = vm
+        .session(base, CHAIN_ID, 1)
         .expect("Instantiating new session should succeed");
 
     // check that the moonlight account is initialized as expected
diff --git a/contracts/stake/tests/stake.rs b/contracts/stake/tests/stake.rs
index aed9e0f1c0..18ba44bc02 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, 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");
@@ -178,11 +177,12 @@ fn stake_withdraw_unstake() {
     // set different block height so that the new notes are easily located and
     // filtered
     let base = session.commit().expect("Committing should succeed");
-    let mut session = new_session(vm, base, CHAIN_ID, 2)
+    let mut session = vm
+        .session(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");
@@ -274,11 +274,12 @@ fn stake_withdraw_unstake() {
     // filtered
     // sets the block height for all subsequent operations to 1
     let base = session.commit().expect("Committing should succeed");
-    let mut session = new_session(vm, base, CHAIN_ID, 3)
+    let mut session = vm
+        .session(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;
diff --git a/contracts/transfer/tests/common/utils.rs b/contracts/transfer/tests/common/utils.rs
index c42bfd22b5..82f26c52e8 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<u8, VMError> {
         .map(|r| r.data)
 }
 
-/// Executes a transaction.
-/// Returns result containing gas spent.
-pub fn execute(
-    session: &mut Session,
-    tx: impl Into<Transaction>,
-) -> Result<CallReceipt<Result<Vec<u8>, ContractError>>, VMError> {
-    let tx = tx.into();
-
-    let mut receipt = session.call::<_, Result<Vec<u8>, 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 5f8f0dfc19..b2ab55c4fa 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,19 @@ 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, 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;
@@ -70,7 +68,7 @@ fn instantiate(moonlight_pk: &AccountPublicKey) -> Session {
 
     let vm = &mut VM::ephemeral().expect("Creating ephemeral VM should work");
 
-    let mut session = new_genesis_session(vm, CHAIN_ID);
+    let mut session = vm.genesis_session(CHAIN_ID);
 
     session
         .deploy(
@@ -125,7 +123,8 @@ fn instantiate(moonlight_pk: &AccountPublicKey) -> Session {
     // operations to 1
     let base = session.commit().expect("Committing should succeed");
     // start a new session from that base-commit
-    let mut session = new_session(vm, base, CHAIN_ID, 1)
+    let mut session = vm
+        .session(base, CHAIN_ID, 1)
         .expect("Instantiating new session should succeed");
 
     // check genesis state
@@ -171,7 +170,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 +183,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 +232,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 +244,11 @@ fn transfer_with_refund() {
         CHAIN_ID,
         None::<TransactionData>,
     )
-    .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 +300,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 +313,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 +352,7 @@ fn alice_ping() {
         fn_args: vec![],
     });
 
-    let transaction = MoonlightTransaction::new(
+    let transaction = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -365,7 +365,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 +432,7 @@ fn convert_to_phoenix() {
         .to_vec(),
     };
 
-    let tx = MoonlightTransaction::new(
+    let tx = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -446,7 +446,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 +552,7 @@ fn convert_to_moonlight_fails() {
         .to_vec(),
     };
 
-    let tx = MoonlightTransaction::new(
+    let tx = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -566,8 +566,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 +659,7 @@ fn convert_wrong_contract_targeted() {
         .to_vec(),
     };
 
-    let tx = MoonlightTransaction::new(
+    let tx = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -672,7 +672,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 +731,7 @@ fn contract_to_contract() {
         .to_vec(),
     });
 
-    let transaction = MoonlightTransaction::new(
+    let transaction = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -744,8 +744,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 +798,7 @@ fn contract_to_account() {
         .to_vec(),
     });
 
-    let transaction = MoonlightTransaction::new(
+    let transaction = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -811,8 +811,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 +862,7 @@ fn contract_to_account_insufficient_funds() {
         .to_vec(),
     });
 
-    let transaction = MoonlightTransaction::new(
+    let transaction = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -875,8 +875,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 +933,7 @@ fn contract_to_account_direct_call() {
         .to_vec(),
     });
 
-    let transaction = MoonlightTransaction::new(
+    let transaction = Transaction::moonlight(
         &moonlight_sk,
         None,
         0,
@@ -946,8 +946,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 97496751cb..e481c57164 100644
--- a/contracts/transfer/tests/phoenix.rs
+++ b/contracts/transfer/tests/phoenix.rs
@@ -24,20 +24,17 @@ 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,
-};
+use dusk_vm::{execute, ContractData, Error as VMError, Session, VM};
 use ff::Field;
 use rand::rngs::StdRng;
 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,
 };
@@ -84,7 +81,7 @@ fn instantiate<const N: u8>(
 
     let vm = &mut VM::ephemeral().expect("Creating ephemeral VM should work");
 
-    let mut session = new_genesis_session(vm, CHAIN_ID);
+    let mut session = vm.genesis_session(CHAIN_ID);
 
     session
         .deploy(
@@ -160,7 +157,8 @@ fn instantiate<const N: u8>(
     // operations to 1
     let base = session.commit().expect("Committing should succeed");
     // start a new session from that base-commit
-    let mut session = new_session(vm, base, CHAIN_ID, 1)
+    let mut session = vm
+        .session(base, CHAIN_ID, 1)
         .expect("Instantiating new session should succeed");
 
     // check that the genesis state is correct:
@@ -254,7 +252,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 +359,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 +467,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 +575,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 +679,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 +750,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 +820,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 +833,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 +928,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 +1054,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 +1175,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 +1285,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 +1376,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 +1470,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 +1586,7 @@ fn create_phoenix_transaction<const I: usize>(
     obfuscated_transaction: bool,
     deposit: u64,
     data: Option<impl Into<TransactionData>>,
-) -> PhoenixTransaction {
+) -> Transaction {
     // Get the root of the tree of phoenix-notes.
     let root = root(session).expect("Getting the anchor should be successful");
 
@@ -1629,4 +1630,5 @@ fn create_phoenix_transaction<const I: usize>(
         &LocalProver,
     )
     .expect("creating the creation shouldn't fail")
+    .into()
 }
diff --git a/rusk/tests/rusk-state.rs b/rusk/tests/rusk-state.rs
index 505978624d..d9d687d296 100644
--- a/rusk/tests/rusk-state.rs
+++ b/rusk/tests/rusk-state.rs
@@ -83,9 +83,9 @@ where
 
     rusk.with_tip(|mut tip, vm| {
         let current_commit = tip.current;
-        let mut session =
-            dusk_vm::new_session(vm, current_commit, CHAIN_ID, BLOCK_HEIGHT)
-                .expect("current commit should exist");
+        let mut session = vm
+            .session(current_commit, CHAIN_ID, BLOCK_HEIGHT)
+            .expect("current commit should exist");
 
         session
             .call::<_, Note>(
diff --git a/rusk/tests/services/contract_deployment.rs b/rusk/tests/services/contract_deployment.rs
index 83c0fdd09d..66701e8374 100644
--- a/rusk/tests/services/contract_deployment.rs
+++ b/rusk/tests/services/contract_deployment.rs
@@ -12,10 +12,9 @@ use dusk_core::abi::ContractId;
 use dusk_core::transfer::data::{
     ContractBytecode, ContractDeploy, TransactionData,
 };
-use dusk_vm::{new_session, ContractData, Error as VMError, VM};
+use dusk_vm::{gen_contract_id, 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;
@@ -232,7 +231,8 @@ impl Fixture {
         let commit = self.rusk.state_root();
         let vm =
             VM::new(self.path.as_path()).expect("VM creation should succeed");
-        let mut session = new_session(&vm, commit, CHAIN_ID, 0)
+        let mut session = vm
+            .session(commit, CHAIN_ID, 0)
             .expect("Session creation should succeed");
         let result = session.call::<_, u64>(
             self.contract_id,
@@ -250,7 +250,8 @@ impl Fixture {
         let commit = self.rusk.state_root();
         let vm =
             VM::new(self.path.as_path()).expect("VM creation should succeed");
-        let mut session = new_session(&vm, commit, CHAIN_ID, 0)
+        let mut session = vm
+            .session(commit, CHAIN_ID, 0)
             .expect("Session creation should succeed");
         let result = session.call::<_, u64>(
             self.contract_id,
diff --git a/rusk/tests/services/contract_stake.rs b/rusk/tests/services/contract_stake.rs
index 83a853f4b8..90bee3616e 100644
--- a/rusk/tests/services/contract_stake.rs
+++ b/rusk/tests/services/contract_stake.rs
@@ -13,10 +13,10 @@ use dusk_bytes::Serializable;
 use dusk_core::abi::ContractId;
 use dusk_core::transfer::data::ContractCall;
 use dusk_core::transfer::{self, Transaction};
+use dusk_vm::gen_contract_id;
 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 4516141501..a4d450c397 100644
--- a/rusk/tests/services/owner_calls.rs
+++ b/rusk/tests/services/owner_calls.rs
@@ -19,8 +19,7 @@ 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 dusk_vm::{gen_contract_id, CallReceipt, ContractData, Session, VM};
 use rusk::{Error, Result, Rusk};
 use rusk_recovery_tools::state;
 use tempfile::tempdir;
@@ -146,7 +145,8 @@ impl Fixture {
         let commit = self.rusk.state_root();
         let vm =
             VM::new(self.path.as_path()).expect("VM creation should succeed");
-        let mut session = new_session(&vm, commit, CHAIN_ID, 0)
+        let mut session = vm
+            .session(commit, CHAIN_ID, 0)
             .expect("Session creation should succeed");
         let result = session.call::<_, u64>(
             self.contract_id,
@@ -171,7 +171,7 @@ impl Fixture {
         let vm =
             VM::new(self.path.as_path()).expect("VM creation should succeed");
         self.session = Some(
-            new_session(&vm, commit, CHAIN_ID, 0)
+            vm.session(commit, CHAIN_ID, 0)
                 .expect("Session creation should succeed"),
         );
     }
diff --git a/vm/tests/vm.rs b/vm/tests/vm.rs
index 2a17e6c17d..3478d10c31 100644
--- a/vm/tests/vm.rs
+++ b/vm/tests/vm.rs
@@ -66,7 +66,7 @@ fn instantiate(vm: &VM, height: u64) -> (Session, ContractId) {
         "../../target/dusk/wasm32-unknown-unknown/release/host_fn.wasm"
     );
 
-    let mut session = dusk_vm::new_genesis_session(vm, CHAIN_ID);
+    let mut session = vm.genesis_session(CHAIN_ID);
 
     let contract_id = session
         .deploy(
@@ -78,7 +78,8 @@ fn instantiate(vm: &VM, height: u64) -> (Session, ContractId) {
 
     let base = session.commit().expect("Committing should succeed");
 
-    let session = dusk_vm::new_session(vm, base, CHAIN_ID, height)
+    let session = vm
+        .session(base, CHAIN_ID, height)
         .expect("Instantiating new session should succeed");
 
     (session, contract_id)