diff --git a/rusk/src/lib/chain/rusk.rs b/rusk/src/lib/chain/rusk.rs index 9fbf7f1ab7..9361ecbf09 100644 --- a/rusk/src/lib/chain/rusk.rs +++ b/rusk/src/lib/chain/rusk.rs @@ -19,6 +19,7 @@ use dusk_consensus::operations::{ CallParams, VerificationOutput, VoterWithCredits, }; use execution_core::bytecode::Bytecode; +use execution_core::transfer::ContractDeploy; use execution_core::{ stake::StakeData, transfer::Transaction as PhoenixTransaction, BlsScalar, StakePublicKey, @@ -505,22 +506,87 @@ fn bytecode_charge( * gas_per_deploy_byte.unwrap_or(DEFAULT_GAS_PER_DEPLOY_BYTE) } +// 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: Option, + receipt: &mut CallReceipt, ContractError>>, +) { + let deploy_charge = bytecode_charge(&deploy.bytecode, &gas_per_deploy_byte); + 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(OutOfGas); + } else if hash != deploy.bytecode.hash { + receipt.data = Err(Panic("failed bytecode hash check".into())) + } else { + let result = session.deploy_raw( + None, + deploy.bytecode.bytes.as_slice(), + deploy.constructor_args.clone(), + deploy.owner.clone(), + gas_limit - receipt.gas_spent, + ); + match result { + Ok(_) => receipt.gas_spent += deploy_charge, + Err(err) => { + info!("Tx caused deployment error {err:?}"); + receipt.data = Err(Panic("failed deployment".into())) + } + } + } +} + /// Executes a transaction, returning the receipt of the call and the gas spent. /// The following steps are performed: /// -/// 1. Call the "spend_and_execute" function on the transfer contract with +/// 1. Check if the transaction contains contract deployment data, and if so, +/// verifies if gas limit is enough for deployment. If 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. /// -/// 2. Call the "refund" function on the transfer contract with unlimited gas. +/// 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 step 1. +/// 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: &PhoenixTransaction, gas_per_deploy_byte: Option, ) -> Result, ContractError>>, PiecrustError> { + // Transaction will be discarded if it is a deployment transaction + // with gas limit smaller than deploy charge. if let Some(deploy) = tx.payload().contract_deploy() { let deploy_charge = bytecode_charge(&deploy.bytecode, &gas_per_deploy_byte); @@ -541,35 +607,16 @@ fn execute( tx.payload().fee.gas_limit, )?; - // Deploy if this is a deployment transaction + // Deploy if this is a deployment transaction and spend part is successful. if let Some(deploy) = tx.payload().contract_deploy() { if receipt.data.is_ok() { - let deploy_charge = - bytecode_charge(&deploy.bytecode, &gas_per_deploy_byte); - let min_gas_limit = receipt.gas_spent + deploy_charge; - let hash = blake3::hash(deploy.bytecode.bytes.as_slice()); - if tx.payload().fee.gas_limit < min_gas_limit { - receipt.data = Err(OutOfGas); - } else if hash != deploy.bytecode.hash { - receipt.data = Err(Panic("failed bytecode hash check".into())) - } else { - let result = session.deploy_raw( - None, - deploy.bytecode.bytes.as_slice(), - deploy.constructor_args.clone(), - deploy.owner.clone(), - tx.payload().fee.gas_limit - receipt.gas_spent, - ); - match result { - Ok(_) => { - receipt.gas_spent += deploy_charge; - } - Err(err) => { - info!("Tx caused deployment error {err:?}"); - receipt.data = Err(Panic("failed deployment".into())) - } - } - } + contract_deploy( + session, + deploy, + tx.payload().fee.gas_limit, + gas_per_deploy_byte, + &mut receipt, + ); } };