Skip to main content

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