mod config;
pub mod feature;
use blake2b_simd::Params;
use dusk_core::abi::{CONTRACT_ID_BYTES, ContractError, ContractId, Metadata};
use dusk_core::stake::STAKE_CONTRACT;
use dusk_core::transfer::data::ContractBytecode;
use dusk_core::transfer::{TRANSFER_CONTRACT, Transaction};
use piecrust::{CallReceipt, Error, Session};
use wasmparser::*;
pub use config::Config;
pub fn execute(
session: &mut Session,
tx: &Transaction,
config: &Config,
) -> Result<CallReceipt<Result<Vec<u8>, ContractError>>, Error> {
tx.phoenix_fee_check()
.map_err(|e| Error::Panic(e.legacy_to_string()))?;
if config.phoenix_refund_check {
tx.phoenix_refund_check()
.map_err(|e| Error::Panic(e.legacy_to_string()))?;
}
tx.deploy_check(
config.gas_per_deploy_byte,
config.min_deploy_gas_price,
config.min_deploy_points,
)
.map_err(|e| Error::Panic(e.legacy_to_string()))?;
if let Some(contract_deploy) = tx.deploy() {
match (config.disable_wasm32, config.disable_wasm64) {
(true, true) => Err(Error::Panic(
"contract deployment is not enabled in the VM".into(),
)),
(true, false) if !is_wasm64(&contract_deploy.bytecode.bytes) => {
Err(Error::Panic("32-bit wasm is not enabled in the VM".into()))
}
(false, true) if is_wasm64(&contract_deploy.bytecode.bytes) => {
Err(Error::Panic("64-bit wasm is not enabled in the VM".into()))
}
_ => Ok(()),
}?
}
if config.disable_3rd_party {
if let Some(call) = tx.call() {
if call.contract != TRANSFER_CONTRACT
&& call.contract != STAKE_CONTRACT
{
return Err(Error::Panic(
"3rd party contracts are not enabled in the VM".into(),
));
}
}
}
let blob_min_charge = tx
.blob_check(config.gas_per_blob)
.map_err(|e| Error::Panic(e.legacy_to_string()))?;
if blob_min_charge.is_some() && !config.with_blob {
return Err(Error::Panic(
"Blob processing is not enabled in the VM".into(),
));
}
if config.with_public_sender {
let _ = session
.set_meta(Metadata::PUBLIC_SENDER, tx.moonlight_sender().copied());
}
let stripped_tx = tx.blob_to_memo().or(tx.strip_off_bytecode());
let mut receipt = session
.call::<_, Result<Vec<u8>, ContractError>>(
TRANSFER_CONTRACT,
"spend_and_execute",
stripped_tx.as_ref().unwrap_or(tx),
tx.gas_limit(),
)
.inspect_err(|_| {
clear_session(session, config);
})?;
contract_deploy(session, tx, config, &mut receipt);
if let Some(blob_min_charge) = blob_min_charge {
if receipt.gas_spent < blob_min_charge {
receipt.gas_spent = blob_min_charge;
}
}
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);
clear_session(session, config);
Ok(receipt)
}
fn is_wasm64(bytecode: &[u8]) -> bool {
for payload in Parser::new(0).parse_all(bytecode).flatten() {
if let Payload::MemorySection(section) = payload {
return section
.into_iter()
.any(|memory| memory.is_ok_and(|m| m.memory64));
}
}
false
}
fn clear_session(session: &mut Session, config: &Config) {
if config.with_public_sender {
let _ = session.remove_meta(Metadata::PUBLIC_SENDER);
}
}
fn contract_deploy(
session: &mut Session,
tx: &Transaction,
config: &Config,
receipt: &mut CallReceipt<Result<Vec<u8>, ContractError>>,
) {
if let Some(deploy) = tx.deploy() {
let gas_per_deploy_byte = config.gas_per_deploy_byte;
let min_deploy_points = config.min_deploy_points;
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 {
Ok(_) => receipt.gas_spent += deploy_charge,
Err(err) => {
let msg = format!("failed deployment: {err:?}");
receipt.data = Err(ContractError::Panic(msg))
}
}
}
}
}
}
fn verify_bytecode_hash(bytecode: &ContractBytecode) -> bool {
let computed: [u8; 32] = blake3::hash(bytecode.bytes.as_slice()).into();
bytecode.hash == computed
}
pub fn gen_contract_id(
bytes: impl AsRef<[u8]>,
nonce: u64,
owner: impl AsRef<[u8]>,
) -> ContractId {
let mut hasher = Params::new().hash_length(CONTRACT_ID_BYTES).to_state();
hasher.update(bytes.as_ref());
hasher.update(&nonce.to_le_bytes()[..]);
hasher.update(owner.as_ref());
let hash_bytes: [u8; CONTRACT_ID_BYTES] = hasher
.finalize()
.as_bytes()
.try_into()
.expect("the hash result is exactly `CONTRACT_ID_BYTES` long");
ContractId::from_bytes(hash_bytes)
}
#[cfg(test)]
mod tests {
use alloc::vec;
use ff as _;
use hex 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
]
);
}
}