dusk_vm/execute.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4//
5// Copyright (c) DUSK NETWORK. All rights reserved.
6
7mod config;
8
9use blake2b_simd::Params;
10use dusk_core::abi::{ContractError, ContractId, Metadata, CONTRACT_ID_BYTES};
11use dusk_core::transfer::data::ContractBytecode;
12use dusk_core::transfer::{Transaction, TRANSFER_CONTRACT};
13use piecrust::{CallReceipt, Error, Session};
14
15pub use config::Config;
16
17/// Executes a transaction in the provided session.
18///
19/// This function processes the transaction, invoking smart contracts or
20/// updating state.
21///
22/// During the execution the following steps are performed:
23///
24/// 1. Check if the transaction contains contract deployment data, and if so,
25/// verifies if gas limit is enough for deployment and if the gas price is
26/// sufficient for deployment. If either gas price or gas limit is not
27/// sufficient for deployment, transaction is discarded.
28///
29/// 2. Call the "spend_and_execute" function on the transfer contract with
30/// unlimited gas. If this fails, an error is returned. If an error is
31/// returned the transaction should be considered unspendable/invalid, but no
32/// re-execution of previous transactions is required.
33///
34/// 3. If the transaction contains contract deployment data, additional checks
35/// are performed and if they pass, deployment is executed. The following
36/// checks are performed:
37/// - gas limit should be is smaller than deploy charge plus gas used for
38/// spending funds
39/// - transaction's bytecode's bytes are consistent with bytecode's hash
40/// Deployment execution may fail for deployment-specific reasons, such as
41/// for example:
42/// - contract already deployed
43/// - corrupted bytecode
44/// If deployment execution fails, the entire gas limit is consumed and error
45/// is returned.
46///
47/// 4. Call the "refund" function on the transfer contract with unlimited gas.
48/// The amount charged depends on the gas spent by the transaction, and the
49/// optional contract call in steps 2 or 3.
50///
51/// Note that deployment transaction will never be re-executed for reasons
52/// related to deployment, as it is either discarded or it charges the
53/// full gas limit. It might be re-executed only if some other transaction
54/// failed to fit the block.
55///
56/// # Arguments
57/// * `session` - A mutable reference to the session executing the transaction.
58/// * `tx` - The transaction to execute.
59/// * `config` - The configuration for the execution of the transaction.
60///
61/// # Returns
62/// A result indicating success or failure.
63pub fn execute(
64 session: &mut Session,
65 tx: &Transaction,
66 config: &Config,
67) -> Result<CallReceipt<Result<Vec<u8>, ContractError>>, Error> {
68 // Transaction will be discarded if it is a deployment transaction
69 // with gas limit smaller than deploy charge.
70 deploy_check(tx, config)?;
71
72 if config.with_public_sender {
73 let _ = session
74 .set_meta(Metadata::PUBLIC_SENDER, tx.moonlight_sender().copied());
75 }
76
77 // Spend the inputs and execute the call. If this errors the transaction is
78 // unspendable.
79 let mut receipt = session
80 .call::<_, Result<Vec<u8>, ContractError>>(
81 TRANSFER_CONTRACT,
82 "spend_and_execute",
83 tx.strip_off_bytecode().as_ref().unwrap_or(tx),
84 tx.gas_limit(),
85 )
86 .map_err(|e| {
87 clear_session(session, config);
88 e
89 })?;
90
91 // Deploy if this is a deployment transaction and spend part is successful.
92 contract_deploy(session, tx, config, &mut receipt);
93
94 // Ensure all gas is consumed if there's an error in the contract call
95 if receipt.data.is_err() {
96 receipt.gas_spent = receipt.gas_limit;
97 }
98
99 // Refund the appropriate amount to the transaction. This call is guaranteed
100 // to never error. If it does, then a programming error has occurred. As
101 // such, the call to `Result::expect` is warranted.
102 let refund_receipt = session
103 .call::<_, ()>(
104 TRANSFER_CONTRACT,
105 "refund",
106 &receipt.gas_spent,
107 u64::MAX,
108 )
109 .expect("Refunding must succeed");
110
111 receipt.events.extend(refund_receipt.events);
112
113 clear_session(session, config);
114
115 Ok(receipt)
116}
117
118fn clear_session(session: &mut Session, config: &Config) {
119 if config.with_public_sender {
120 let _ = session.remove_meta(Metadata::PUBLIC_SENDER);
121 }
122}
123
124fn deploy_check(tx: &Transaction, config: &Config) -> Result<(), Error> {
125 if tx.deploy().is_some() {
126 let gas_per_deploy_byte = config.gas_per_deploy_byte;
127 let min_deploy_gas_price = config.min_deploy_gas_price;
128 let deploy_charge =
129 tx.deploy_charge(gas_per_deploy_byte, min_deploy_gas_price);
130
131 if tx.gas_price() < min_deploy_gas_price {
132 return Err(Error::Panic("gas price too low to deploy".into()));
133 }
134 if tx.gas_limit() < deploy_charge {
135 return Err(Error::Panic("not enough gas to deploy".into()));
136 }
137 }
138
139 Ok(())
140}
141
142// Contract deployment will fail and charge full gas limit in the
143// following cases:
144// 1) Transaction gas limit is smaller than deploy charge plus gas used for
145// spending funds.
146// 2) Transaction's bytecode's bytes are not consistent with bytecode's hash.
147// 3) Deployment fails for deploy-specific reasons like e.g.:
148// - contract already deployed
149// - corrupted bytecode
150// - sufficient gas to spend funds yet insufficient for deployment
151fn contract_deploy(
152 session: &mut Session,
153 tx: &Transaction,
154 config: &Config,
155 receipt: &mut CallReceipt<Result<Vec<u8>, ContractError>>,
156) {
157 if let Some(deploy) = tx.deploy() {
158 let gas_per_deploy_byte = config.gas_per_deploy_byte;
159 let min_deploy_points = config.min_deploy_points;
160
161 let gas_left = tx.gas_limit() - receipt.gas_spent;
162 if receipt.data.is_ok() {
163 let deploy_charge =
164 tx.deploy_charge(gas_per_deploy_byte, min_deploy_points);
165 let min_gas_limit = receipt.gas_spent + deploy_charge;
166 if gas_left < min_gas_limit {
167 receipt.data = Err(ContractError::OutOfGas);
168 } else if !verify_bytecode_hash(&deploy.bytecode) {
169 receipt.data = Err(ContractError::Panic(
170 "failed bytecode hash check".into(),
171 ))
172 } else {
173 let result = session.deploy_raw(
174 Some(gen_contract_id(
175 &deploy.bytecode.bytes,
176 deploy.nonce,
177 &deploy.owner,
178 )),
179 deploy.bytecode.bytes.as_slice(),
180 deploy.init_args.clone(),
181 deploy.owner.clone(),
182 gas_left,
183 );
184 match result {
185 // Should the gas spent by the INIT method charged too?
186 Ok(_) => receipt.gas_spent += deploy_charge,
187 Err(err) => {
188 let msg = format!("failed deployment: {err:?}");
189 receipt.data = Err(ContractError::Panic(msg))
190 }
191 }
192 }
193 }
194 }
195}
196
197// Verifies that the stored contract bytecode hash is correct.
198fn verify_bytecode_hash(bytecode: &ContractBytecode) -> bool {
199 let computed: [u8; 32] = blake3::hash(bytecode.bytes.as_slice()).into();
200
201 bytecode.hash == computed
202}
203
204/// Generates a unique identifier for a smart contract.
205///
206/// # Arguments
207/// * 'bytes` - The contract bytecode.
208/// * `nonce` - A unique nonce.
209/// * `owner` - The contract-owner.
210///
211/// # Returns
212/// A unique [`ContractId`].
213///
214/// # Panics
215/// Panics if [blake2b-hasher] doesn't produce a [`CONTRACT_ID_BYTES`]
216/// bytes long hash.
217///
218/// [blake2b-hasher]: [`blake2b_simd::Params.finalize`]
219pub fn gen_contract_id(
220 bytes: impl AsRef<[u8]>,
221 nonce: u64,
222 owner: impl AsRef<[u8]>,
223) -> ContractId {
224 let mut hasher = Params::new().hash_length(CONTRACT_ID_BYTES).to_state();
225 hasher.update(bytes.as_ref());
226 hasher.update(&nonce.to_le_bytes()[..]);
227 hasher.update(owner.as_ref());
228 let hash_bytes: [u8; CONTRACT_ID_BYTES] = hasher
229 .finalize()
230 .as_bytes()
231 .try_into()
232 .expect("the hash result is exactly `CONTRACT_ID_BYTES` long");
233 ContractId::from_bytes(hash_bytes)
234}
235
236#[cfg(test)]
237mod tests {
238 use alloc::vec;
239
240 // the `unused_crate_dependencies` lint complains for dev-dependencies that
241 // are only used in integration tests, so adding this work-around here
242 use ff as _;
243 use once_cell as _;
244 use rand::rngs::StdRng;
245 use rand::{RngCore, SeedableRng};
246
247 use super::*;
248
249 #[test]
250 fn test_gen_contract_id() {
251 let mut rng = StdRng::seed_from_u64(42);
252
253 let mut bytes = vec![0; 1000];
254 rng.fill_bytes(&mut bytes);
255
256 let nonce = rng.next_u64();
257
258 let mut owner = vec![0, 100];
259 rng.fill_bytes(&mut owner);
260
261 let contract_id =
262 gen_contract_id(bytes.as_slice(), nonce, owner.as_slice());
263
264 assert_eq!(
265 contract_id.as_bytes(),
266 [
267 45, 168, 182, 39, 119, 137, 168, 140, 114, 21, 120, 158, 34,
268 126, 244, 221, 151, 72, 109, 178, 82, 229, 84, 128, 92, 123,
269 135, 74, 23, 224, 119, 133
270 ]
271 );
272 }
273}