1mod config;
8pub mod feature;
9
10pub use config::Config;
11use dusk_core::abi::{ContractError, ContractId, Metadata};
12use dusk_core::stake::STAKE_CONTRACT;
13use dusk_core::transfer::data::{ContractBytecode, gen_contract_id};
14use dusk_core::transfer::withdraw::{
15 Withdraw, WithdrawReceiver, WithdrawReplayToken,
16};
17use dusk_core::transfer::{TRANSFER_CONTRACT, Transaction};
18use piecrust::{CallReceipt, Session};
19use rkyv::Deserialize;
20use wasmparser::*;
21
22use crate::ExecutionError;
23
24const DEPLOY_FEATURE_VALIDATION_ERROR: &str =
25 "failed deployment: bytecode validation rejected";
26const PHOENIX_DISABLED_ERROR: &str = "phoenix is not enabled in the VM";
27const TRANSFER_WITHDRAWAL_FUNCTIONS: &[&str] = &["mint", "withdraw", "convert"];
28
29pub fn execute(
80 session: &mut Session,
81 tx: &Transaction,
82 config: &Config,
83) -> Result<CallReceipt<Result<Vec<u8>, ContractError>>, ExecutionError> {
84 if config.disable_phoenix && matches!(tx, Transaction::Phoenix(_)) {
85 return Err(ExecutionError::precondition(PHOENIX_DISABLED_ERROR));
86 }
87
88 tx.phoenix_fee_check()?;
89
90 if config.phoenix_refund_check {
91 tx.phoenix_refund_check()?;
92 }
93
94 tx.deploy_check(
97 config.gas_per_deploy_byte,
98 config.min_deploy_gas_price,
99 config.min_deploy_points,
100 )?;
101
102 if let Some(contract_deploy) = tx.deploy() {
103 let is_wasm64 = is_wasm64(&contract_deploy.bytecode.bytes);
104 match (config.disable_wasm32, config.disable_wasm64) {
105 (true, true) => Err(ExecutionError::precondition(
106 "contract deployment is not enabled in the VM",
107 )),
108 (true, false) if !is_wasm64 => Err(ExecutionError::precondition(
109 "32-bit wasm is not enabled in the VM",
110 )),
111 (false, true) if is_wasm64 => Err(ExecutionError::precondition(
112 "64-bit wasm is not enabled in the VM",
113 )),
114 _ => Ok(()),
115 }?
116 }
117
118 if config.disable_3rd_party
119 && let Some(call) = tx.call()
120 && call.contract != TRANSFER_CONTRACT
121 && call.contract != STAKE_CONTRACT
122 {
123 return Err(ExecutionError::precondition(
124 "3rd party contracts are not enabled in the VM",
125 ));
126 }
127
128 let blob_min_charge = tx.blob_check(config.gas_per_blob)?;
129
130 if blob_min_charge.is_some() && !config.with_blob {
131 return Err(ExecutionError::precondition(
132 "Blob processing is not enabled in the VM",
133 ));
134 }
135
136 if config.with_public_sender {
137 let _ = session
138 .set_meta(Metadata::PUBLIC_SENDER, tx.moonlight_sender().copied());
139 }
140
141 let stripped_tx = tx.blob_to_memo().or(tx.strip_off_bytecode());
142
143 if (config.disable_phoenix || config.withdrawal_nullifier_check)
153 && tx.call().is_some()
154 {
155 let disable_phoenix = config.disable_phoenix;
156 let withdrawal_nullifier_check = config.withdrawal_nullifier_check;
157 let tx_nullifier_count = tx.nullifiers().len();
158 session.set_call_hook(Box::new(move |callee, fn_name, fn_args| {
159 if disable_phoenix {
160 check_phoenix_disabled_call(callee, fn_name, fn_args)?;
161 }
162 if withdrawal_nullifier_check {
163 check_withdrawal_nullifiers(
164 callee,
165 fn_name,
166 fn_args,
167 tx_nullifier_count,
168 )?;
169 }
170 Ok(())
171 }));
172 }
173
174 let mut receipt = session
177 .call::<_, Result<Vec<u8>, ContractError>>(
178 TRANSFER_CONTRACT,
179 "spend_and_execute",
180 stripped_tx.as_ref().unwrap_or(tx),
181 tx.gas_limit(),
182 )
183 .inspect_err(|_| {
184 clear_session(session, config);
185 })
186 .map_err(ExecutionError::from_spend_and_execute)?;
187
188 contract_deploy(session, tx, config, &mut receipt);
190
191 if let Some(blob_min_charge) = blob_min_charge
194 && receipt.gas_spent < blob_min_charge
195 {
196 receipt.gas_spent = blob_min_charge;
197 }
198
199 if receipt.data.is_err() {
201 receipt.gas_spent = receipt.gas_limit;
202 }
203
204 let refund_receipt = session
208 .call::<_, ()>(
209 TRANSFER_CONTRACT,
210 "refund",
211 &receipt.gas_spent,
212 u64::MAX,
213 )
214 .inspect_err(|_| {
215 clear_session(session, config);
216 })
217 .map_err(ExecutionError::FailedRefund)?;
218
219 receipt.events.extend(refund_receipt.events);
220
221 clear_session(session, config);
222
223 Ok(receipt)
224}
225
226fn check_phoenix_disabled_call(
227 callee: &ContractId,
228 fn_name: &str,
229 fn_args: &[u8],
230) -> Result<(), String> {
231 if *callee != TRANSFER_CONTRACT
232 || !TRANSFER_WITHDRAWAL_FUNCTIONS.contains(&fn_name)
233 {
234 return Ok(());
235 }
236 if fn_name == "convert" {
237 return Err(PHOENIX_DISABLED_ERROR.into());
242 }
243 let withdraw = deserialize_withdraw(fn_args)?;
244 if withdraw_uses_phoenix(&withdraw) {
245 return Err(PHOENIX_DISABLED_ERROR.into());
246 }
247 Ok(())
248}
249
250fn withdraw_uses_phoenix(withdraw: &Withdraw) -> bool {
251 matches!(withdraw.receiver(), WithdrawReceiver::Phoenix(_))
252 || matches!(withdraw.token(), WithdrawReplayToken::Phoenix(_))
253}
254
255fn deserialize_withdraw(fn_args: &[u8]) -> Result<Withdraw, String> {
256 let Ok(root) = rkyv::check_archived_root::<Withdraw>(fn_args) else {
257 return Err("failed to deserialize withdrawal arguments".into());
258 };
259 match root.deserialize(&mut rkyv::Infallible) {
260 Ok(w) => Ok(w),
261 Err(infallible) => match infallible {},
262 }
263}
264
265fn is_wasm64(bytecode: &[u8]) -> bool {
266 for payload in Parser::new(0).parse_all(bytecode).flatten() {
267 if let Payload::MemorySection(section) = payload {
268 return section
269 .into_iter()
270 .any(|memory| memory.is_ok_and(|m| m.memory64));
271 }
272 }
273 false
274}
275
276fn clear_session(session: &mut Session, config: &Config) {
277 if config.with_public_sender {
278 let _ = session.remove_meta(Metadata::PUBLIC_SENDER);
279 }
280 session.clear_call_hook();
281}
282
283fn check_withdrawal_nullifiers(
290 callee: &ContractId,
291 fn_name: &str,
292 fn_args: &[u8],
293 tx_nullifier_count: usize,
294) -> Result<(), String> {
295 if *callee != TRANSFER_CONTRACT || fn_name != "withdraw" {
296 return Ok(());
297 }
298 let withdraw = deserialize_withdraw(fn_args)?;
299 if let WithdrawReplayToken::Phoenix(nullifiers) = withdraw.token()
300 && nullifiers.len() != tx_nullifier_count
301 {
302 return Err(format!(
303 "nullifier count mismatch: withdrawal has {}, transaction has {}",
304 nullifiers.len(),
305 tx_nullifier_count,
306 ));
307 }
308 Ok(())
309}
310
311fn contract_deploy(
322 session: &mut Session,
323 tx: &Transaction,
324 config: &Config,
325 receipt: &mut CallReceipt<Result<Vec<u8>, ContractError>>,
326) {
327 if let Some(deploy) = tx.deploy() {
328 let gas_per_deploy_byte = config.gas_per_deploy_byte;
329 let min_deploy_points = config.min_deploy_points;
330
331 if receipt.data.is_ok() {
332 let Ok(deploy_charge) =
333 tx.deploy_charge(gas_per_deploy_byte, min_deploy_points)
334 else {
335 receipt.data =
336 Err(ContractError::Panic("deploy charge overflow".into()));
337 return;
338 };
339 if !is_deploy_gas_sufficient(
340 tx.gas_limit(),
341 receipt.gas_spent,
342 deploy_charge,
343 config.deploy_remaining_gas_check,
344 ) {
345 receipt.data = Err(ContractError::OutOfGas);
346 } else if !verify_bytecode_hash(&deploy.bytecode) {
347 receipt.data = Err(ContractError::Panic(
348 "failed bytecode hash check".into(),
349 ))
350 } else if let Err(err) = validate_deploy_bytecode_features(
351 &deploy.bytecode.bytes,
352 config.with_reference_types,
353 ) {
354 receipt.data = Err(ContractError::Panic(err.into()))
355 } else {
356 let gas_left = tx.gas_limit().saturating_sub(receipt.gas_spent);
357 let init_budget = if config.charge_init_gas {
358 gas_left.saturating_sub(deploy_charge)
359 } else {
360 gas_left
361 };
362 let result = session.deploy_raw(
363 Some(gen_contract_id(
364 &deploy.bytecode.bytes,
365 deploy.nonce,
366 &deploy.owner,
367 )),
368 deploy.bytecode.bytes.as_slice(),
369 deploy.init_args.clone(),
370 deploy.owner.clone(),
371 init_budget,
372 );
373 match result {
374 Ok((_, init_receipt)) => {
375 receipt.gas_spent =
376 receipt.gas_spent.saturating_add(deploy_charge);
377 apply_deploy_init_receipt(
378 receipt,
379 init_receipt,
380 config.charge_init_gas,
381 );
382 }
383 Err(err) => {
384 let msg = format!("failed deployment: {err:?}");
385 receipt.data = Err(ContractError::Panic(msg))
386 }
387 }
388 }
389 }
390 }
391}
392
393fn validate_deploy_bytecode_features(
394 bytecode: &[u8],
395 with_reference_types: bool,
396) -> Result<(), &'static str> {
397 if with_reference_types {
398 return Ok(());
399 }
400
401 Validator::new_with_features(pre_reference_types_deploy_features())
402 .validate_all(bytecode)
403 .map(|_| ())
404 .map_err(|_| DEPLOY_FEATURE_VALIDATION_ERROR)
405}
406
407fn pre_reference_types_deploy_features() -> WasmFeatures {
408 WasmFeatures::WASM2
414 .difference(WasmFeatures::REFERENCE_TYPES)
415 .union(WasmFeatures::RELAXED_SIMD)
416 .union(WasmFeatures::MULTI_MEMORY)
417 .union(WasmFeatures::MEMORY64)
418}
419
420fn apply_deploy_init_receipt(
421 receipt: &mut CallReceipt<Result<Vec<u8>, ContractError>>,
422 init_receipt: Option<CallReceipt<Vec<u8>>>,
423 charge_init_gas: bool,
424) {
425 if let Some(init_receipt) = init_receipt {
426 if charge_init_gas {
427 receipt.gas_spent =
428 receipt.gas_spent.saturating_add(init_receipt.gas_spent);
429 }
430 receipt.events.extend(init_receipt.events);
431 }
432}
433
434fn is_deploy_gas_sufficient(
435 gas_limit: u64,
436 gas_spent: u64,
437 deploy_charge: u64,
438 deploy_remaining_gas_check: bool,
439) -> bool {
440 let gas_left = gas_limit.saturating_sub(gas_spent);
441
442 if deploy_remaining_gas_check {
443 gas_left >= deploy_charge
444 } else {
445 gas_spent
446 .checked_add(deploy_charge)
447 .is_some_and(|required| gas_left >= required)
448 }
449}
450
451fn verify_bytecode_hash(bytecode: &ContractBytecode) -> bool {
453 let computed: [u8; 32] = blake3::hash(bytecode.bytes.as_slice()).into();
454
455 bytecode.hash == computed
456}
457
458#[cfg(test)]
459mod tests {
460 use alloc::vec;
461
462 use dusk_core::BlsScalar;
463 use dusk_core::abi::{ContractId, Event};
464 use rand::rngs::StdRng;
465 use rand::{RngCore, SeedableRng};
466 use {ff as _, hex as _, once_cell as _};
469
470 use super::*;
471 use crate::CallTree;
472
473 #[test]
474 fn check_withdrawal_nullifiers_matching_count_passes() {
475 let rng = &mut StdRng::seed_from_u64(0xbeef);
476
477 let note_sk = dusk_core::signatures::schnorr::SecretKey::random(rng);
478 let note_pk = dusk_core::signatures::schnorr::PublicKey::from(¬e_sk);
479 let address =
480 dusk_core::transfer::phoenix::StealthAddress::from_raw_unchecked(
481 *note_pk.as_ref(),
482 note_pk,
483 );
484
485 let nullifiers = vec![BlsScalar::from(1), BlsScalar::from(2)];
486 let withdraw = dusk_core::transfer::withdraw::Withdraw::new(
487 rng,
488 ¬e_sk,
489 TRANSFER_CONTRACT,
490 100,
491 dusk_core::transfer::withdraw::WithdrawReceiver::Phoenix(address),
492 dusk_core::transfer::withdraw::WithdrawReplayToken::Phoenix(
493 nullifiers.clone(),
494 ),
495 );
496
497 let args =
498 rkyv::to_bytes::<_, 4096>(&withdraw).expect("should serialize");
499
500 assert!(
501 check_withdrawal_nullifiers(
502 &TRANSFER_CONTRACT,
503 "withdraw",
504 &args,
505 nullifiers.len(),
506 )
507 .is_ok()
508 );
509 }
510
511 #[test]
512 fn check_withdrawal_nullifiers_mismatched_count_rejects() {
513 let rng = &mut StdRng::seed_from_u64(0xbeef);
514
515 let note_sk = dusk_core::signatures::schnorr::SecretKey::random(rng);
516 let note_pk = dusk_core::signatures::schnorr::PublicKey::from(¬e_sk);
517 let address =
518 dusk_core::transfer::phoenix::StealthAddress::from_raw_unchecked(
519 *note_pk.as_ref(),
520 note_pk,
521 );
522
523 let nullifiers = vec![BlsScalar::from(1), BlsScalar::from(2)];
524 let withdraw = dusk_core::transfer::withdraw::Withdraw::new(
525 rng,
526 ¬e_sk,
527 TRANSFER_CONTRACT,
528 100,
529 dusk_core::transfer::withdraw::WithdrawReceiver::Phoenix(address),
530 dusk_core::transfer::withdraw::WithdrawReplayToken::Phoenix(
531 nullifiers,
532 ),
533 );
534
535 let args =
536 rkyv::to_bytes::<_, 4096>(&withdraw).expect("should serialize");
537
538 let err = check_withdrawal_nullifiers(
540 &TRANSFER_CONTRACT,
541 "withdraw",
542 &args,
543 3,
544 )
545 .unwrap_err();
546 assert!(
547 err.contains("nullifier count mismatch"),
548 "expected mismatch message, got: {err}"
549 );
550 assert!(err.contains("2") && err.contains("3"));
551 }
552
553 #[test]
554 fn check_withdrawal_nullifiers_ignores_non_withdraw_calls() {
555 assert!(
556 check_withdrawal_nullifiers(&TRANSFER_CONTRACT, "refund", &[], 5,)
557 .is_ok()
558 );
559
560 assert!(
561 check_withdrawal_nullifiers(
562 &ContractId::from_bytes([0xAA; 32]),
563 "withdraw",
564 &[],
565 5,
566 )
567 .is_ok()
568 );
569 }
570
571 #[test]
572 fn check_withdrawal_nullifiers_rejects_garbage_args() {
573 let err = check_withdrawal_nullifiers(
576 &TRANSFER_CONTRACT,
577 "withdraw",
578 &[0xDE, 0xAD, 0xBE, 0xEF],
579 2,
580 )
581 .unwrap_err();
582 assert!(err.contains("deserialize"));
583
584 check_withdrawal_nullifiers(&TRANSFER_CONTRACT, "withdraw", &[], 1)
586 .unwrap_err();
587 }
588
589 #[test]
590 fn check_withdrawal_nullifiers_ignores_moonlight_token() {
591 let rng = &mut StdRng::seed_from_u64(0xdead);
592
593 let moonlight_sk = dusk_core::signatures::bls::SecretKey::random(rng);
594 let moonlight_pk =
595 dusk_core::signatures::bls::PublicKey::from(&moonlight_sk);
596
597 let withdraw = dusk_core::transfer::withdraw::Withdraw::new(
598 rng,
599 &moonlight_sk,
600 TRANSFER_CONTRACT,
601 100,
602 dusk_core::transfer::withdraw::WithdrawReceiver::Moonlight(
603 moonlight_pk,
604 ),
605 dusk_core::transfer::withdraw::WithdrawReplayToken::Moonlight(42),
606 );
607
608 let args =
609 rkyv::to_bytes::<_, 4096>(&withdraw).expect("should serialize");
610
611 assert!(
613 check_withdrawal_nullifiers(
614 &TRANSFER_CONTRACT,
615 "withdraw",
616 &args,
617 999,
618 )
619 .is_ok()
620 );
621 }
622
623 #[test]
624 fn test_gen_contract_id() {
625 let mut rng = StdRng::seed_from_u64(42);
626
627 let mut bytes = vec![0; 1000];
628 rng.fill_bytes(&mut bytes);
629
630 let nonce = rng.next_u64();
631
632 let mut owner = vec![0, 100];
633 rng.fill_bytes(&mut owner);
634
635 let contract_id =
636 gen_contract_id(bytes.as_slice(), nonce, owner.as_slice());
637
638 assert_eq!(
639 contract_id.as_bytes(),
640 [
641 45, 168, 182, 39, 119, 137, 168, 140, 114, 21, 120, 158, 34,
642 126, 244, 221, 151, 72, 109, 178, 82, 229, 84, 128, 92, 123,
643 135, 74, 23, 224, 119, 133
644 ]
645 );
646 }
647
648 #[test]
649 fn deploy_gas_check_matches_prefork_and_boreas_rules() {
650 for (gas_limit, gas_spent, deploy_charge, boreas, expected) in [
651 (10_000_000, 3_000_000, 5_000_000, false, false),
652 (10_000_000, 3_000_000, 5_000_000, true, true),
653 (7_000_000, 3_000_000, 5_000_000, false, false),
654 (7_000_000, 3_000_000, 5_000_000, true, false),
655 (u64::MAX, u64::MAX, 1, false, false),
656 ] {
657 assert_eq!(
658 is_deploy_gas_sufficient(
659 gas_limit,
660 gas_spent,
661 deploy_charge,
662 boreas,
663 ),
664 expected,
665 );
666 }
667 }
668
669 #[test]
670 fn deploy_bytecode_reference_types_are_height_gated() {
671 const EMPTY_MODULE: &[u8] = b"\0asm\x01\0\0\0";
672 const FUNC_WITH_EXTERNREF_MODULE: &[u8] = &[
673 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x05, 0x01, 0x60, 0x01, 0x6f, 0x00, ];
683 const TABLE_WITH_EXTERNREF_MODULE: &[u8] = &[
684 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x04, 0x04, 0x01, 0x6f, 0x00, 0x01, ];
693
694 validate_deploy_bytecode_features(EMPTY_MODULE, false)
695 .expect("MVP bytecode should validate without reference-types");
696
697 let err = validate_deploy_bytecode_features(
698 FUNC_WITH_EXTERNREF_MODULE,
699 false,
700 )
701 .expect_err("reference-types bytecode should fail before activation");
702 assert_eq!(err, DEPLOY_FEATURE_VALIDATION_ERROR);
703
704 let err = validate_deploy_bytecode_features(
705 TABLE_WITH_EXTERNREF_MODULE,
706 false,
707 )
708 .expect_err("reference-types table should fail before activation");
709 assert_eq!(err, DEPLOY_FEATURE_VALIDATION_ERROR);
710
711 Validator::new_with_features(
712 pre_reference_types_deploy_features()
713 .union(WasmFeatures::REFERENCE_TYPES),
714 )
715 .validate_all(FUNC_WITH_EXTERNREF_MODULE)
716 .expect("reference-types bytecode should validate when enabled");
717 }
718
719 #[test]
720 fn pre_reference_types_deploy_features_are_pinned() {
721 let features = pre_reference_types_deploy_features();
722
723 assert!(!features.contains(WasmFeatures::REFERENCE_TYPES));
724 assert!(!features.contains(WasmFeatures::FUNCTION_REFERENCES));
725 assert!(!features.contains(WasmFeatures::GC));
726 assert!(!features.contains(WasmFeatures::THREADS));
727 assert!(!features.contains(WasmFeatures::TAIL_CALL));
728
729 assert!(features.contains(WasmFeatures::BULK_MEMORY));
730 assert!(features.contains(WasmFeatures::MULTI_VALUE));
731 assert!(features.contains(WasmFeatures::SIMD));
732 assert!(features.contains(WasmFeatures::RELAXED_SIMD));
733 assert!(features.contains(WasmFeatures::MULTI_MEMORY));
734 assert!(features.contains(WasmFeatures::MEMORY64));
735 assert!(!features.contains(WasmFeatures::EXCEPTIONS));
736 assert!(!features.contains(WasmFeatures::EXTENDED_CONST));
737 }
738
739 #[test]
740 fn deploy_init_events_are_preserved_before_and_after_boreas() {
741 let init_event = Event {
742 source: ContractId::from_bytes([7; 32]),
743 topic: "runtime_update".into(),
744 data: vec![1, 2, 3, 4],
745 reverted: false,
746 };
747 let build_init_receipt = || CallReceipt {
748 gas_spent: 123,
749 gas_limit: 999,
750 events: vec![init_event.clone()],
751 call_tree: CallTree::default(),
752 data: Vec::new(),
753 };
754
755 let mut prefork_receipt = CallReceipt {
756 gas_spent: 10,
757 gas_limit: 1000,
758 events: vec![],
759 call_tree: CallTree::default(),
760 data: Ok(Vec::new()),
761 };
762 apply_deploy_init_receipt(
763 &mut prefork_receipt,
764 Some(build_init_receipt()),
765 false,
766 );
767 assert_eq!(prefork_receipt.gas_spent, 10);
768 assert_eq!(prefork_receipt.events, vec![init_event.clone()]);
769
770 let mut boreas_receipt = CallReceipt {
771 gas_spent: 10,
772 gas_limit: 1000,
773 events: vec![],
774 call_tree: CallTree::default(),
775 data: Ok(Vec::new()),
776 };
777 apply_deploy_init_receipt(
778 &mut boreas_receipt,
779 Some(build_init_receipt()),
780 true,
781 );
782 assert_eq!(boreas_receipt.gas_spent, 133);
783 assert_eq!(boreas_receipt.events, vec![init_event]);
784 }
785}