From 4f2fcb79dad4f0f182de11cf5ceee7626f61804c Mon Sep 17 00:00:00 2001 From: moana Date: Wed, 13 Nov 2024 11:23:49 +0100 Subject: [PATCH] transfer-contract: Add tests for failing conversions Resolves #2969 --- contracts/transfer/tests/common/utils.rs | 28 ++-- contracts/transfer/tests/moonlight.rs | 139 +++++++++++++++++++- contracts/transfer/tests/phoenix.rs | 155 ++++++++++++++++++++--- 3 files changed, 294 insertions(+), 28 deletions(-) diff --git a/contracts/transfer/tests/common/utils.rs b/contracts/transfer/tests/common/utils.rs index 819f23a3d3..44847090c5 100644 --- a/contracts/transfer/tests/common/utils.rs +++ b/contracts/transfer/tests/common/utils.rs @@ -6,15 +6,13 @@ use std::sync::mpsc; -use execution_core::{ - signatures::bls::PublicKey as AccountPublicKey, - transfer::{ - moonlight::AccountData, - phoenix::{Note, NoteLeaf, ViewKey as PhoenixViewKey}, - Transaction, TRANSFER_CONTRACT, - }, - ContractError, ContractId, +use execution_core::signatures::bls::PublicKey as AccountPublicKey; +use execution_core::transfer::moonlight::AccountData; +use execution_core::transfer::phoenix::{ + Note, NoteLeaf, ViewKey as PhoenixViewKey, }; +use execution_core::transfer::{Transaction, TRANSFER_CONTRACT}; +use execution_core::{BlsScalar, ContractError, ContractId}; use rusk_abi::{CallReceipt, PiecrustError, Session}; const GAS_LIMIT: u64 = 0x10_000_000; @@ -141,3 +139,17 @@ pub fn filter_notes_owned_by>( .filter(|note| vk.owns(note.stealth_address())) .collect() } + +pub fn existing_nullifiers( + session: &mut Session, + nullifiers: &Vec, +) -> Result, PiecrustError> { + session + .call( + TRANSFER_CONTRACT, + "existing_nullifiers", + &nullifiers.clone(), + GAS_LIMIT, + ) + .map(|r| r.data) +} diff --git a/contracts/transfer/tests/moonlight.rs b/contracts/transfer/tests/moonlight.rs index 0358a9c34c..fe9827e796 100644 --- a/contracts/transfer/tests/moonlight.rs +++ b/contracts/transfer/tests/moonlight.rs @@ -7,8 +7,8 @@ pub mod common; use crate::common::utils::{ - account, chain_id, contract_balance, execute, filter_notes_owned_by, - leaves_from_height, owned_notes_value, update_root, + account, chain_id, contract_balance, execute, existing_nullifiers, + filter_notes_owned_by, leaves_from_height, owned_notes_value, update_root, }; use ff::Field; @@ -24,13 +24,13 @@ use execution_core::{ data::{ContractCall, TransactionData}, moonlight::Transaction as MoonlightTransaction, phoenix::{ - PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, + Note, PublicKey as PhoenixPublicKey, SecretKey as PhoenixSecretKey, ViewKey as PhoenixViewKey, }, withdraw::{Withdraw, WithdrawReceiver, WithdrawReplayToken}, ContractToAccount, ContractToContract, TRANSFER_CONTRACT, }, - ContractError, ContractId, JubJubScalar, LUX, + BlsScalar, ContractError, ContractId, JubJubScalar, LUX, }; use rusk_abi::{ContractData, Session}; @@ -478,6 +478,137 @@ fn convert_to_phoenix() { ); } +/// Converting phoenix DUSK into moonlight DUSK with a moonlight transaction +/// should fail. +#[test] +fn convert_to_moonlight_fails() { + const CONVERSION_VALUE: u64 = dusk(10.0); + + let rng = &mut StdRng::seed_from_u64(0xfeeb); + + let phoenix_sk = PhoenixSecretKey::random(rng); + let phoenix_vk = PhoenixViewKey::from(&phoenix_sk); + let phoenix_pk = PhoenixPublicKey::from(&phoenix_sk); + + let moonlight_sk = AccountSecretKey::random(rng); + let moonlight_pk = AccountPublicKey::from(&moonlight_sk); + + let mut session = &mut instantiate(&moonlight_pk); + + // Add a phoenix note with the conversion-value + let value_blinder = JubJubScalar::random(&mut *rng); + let sender_blinder = [ + JubJubScalar::random(&mut *rng), + JubJubScalar::random(&mut *rng), + ]; + let note = Note::obfuscated( + rng, + &phoenix_pk, + &phoenix_pk, + CONVERSION_VALUE, + value_blinder, + sender_blinder, + ); + // get the nullifier for later check + let nullifier = note.gen_nullifier(&phoenix_sk); + // push genesis phoenix note to the contract + session + .call::<_, Note>( + TRANSFER_CONTRACT, + "push_note", + &(0u64, note), + GAS_LIMIT, + ) + .expect("Pushing genesis note should succeed"); + + // update the root after the notes have been inserted + update_root(&mut session).expect("Updating the root should succeed"); + + // make sure that the phoenix-key doesn't own any notes yet + let leaves = leaves_from_height(session, 0) + .expect("getting the notes should succeed"); + let notes = filter_notes_owned_by( + phoenix_vk, + leaves.into_iter().map(|leaf| leaf.note), + ); + assert_eq!(notes.len(), 1, "There should be one note at this height"); + + // a conversion is a deposit into the transfer-contract paired with a + // withdrawal + let contract_call = ContractCall { + contract: TRANSFER_CONTRACT, + fn_name: String::from("convert"), + fn_args: rkyv::to_bytes::<_, 1024>(&Withdraw::new( + rng, + &moonlight_sk, + TRANSFER_CONTRACT, + // set the conversion-value as a withdrawal + CONVERSION_VALUE, + WithdrawReceiver::Moonlight(moonlight_pk), + WithdrawReplayToken::Phoenix(vec![ + notes[0].gen_nullifier(&phoenix_sk) + ]), + )) + .expect("should serialize conversion correctly") + .to_vec(), + }; + + let tx = MoonlightTransaction::new( + &moonlight_sk, + None, + 0, + // set the conversion-value as the deposit + CONVERSION_VALUE, + GAS_LIMIT, + LUX, + MOONLIGHT_GENESIS_NONCE + 1, + CHAIN_ID, + Some(contract_call), + ) + .expect("Creating moonlight transaction should succeed"); + + let receipt = + execute(&mut session, tx).expect("Executing TX should succeed"); + + // check that the transaction execution panicked with the correct message + assert!(receipt.data.is_err()); + assert_eq!( + format!("{}", receipt.data.unwrap_err()), + String::from("Panic: Expected Phoenix TX, found Moonlight"), + "The attempted conversion from phoenix to moonlight when paying gas with moonlight should error" + ); + assert_eq!( + receipt.gas_spent, + GAS_LIMIT * LUX, + "The max gas should have been spent" + ); + + update_root(session).expect("Updating the root should succeed"); + + println!("CONVERT TO MOONLIGHT: {} gas", receipt.gas_spent); + + let moonlight_account = account(&mut session, &moonlight_pk) + .expect("Getting account should succeed"); + + assert_eq!( + moonlight_account.balance, + MOONLIGHT_GENESIS_VALUE - receipt.gas_spent, + "Since the conversion fails, the moonlight account should only have the gas-spent deducted" + ); + + let leaves = leaves_from_height(session, 1) + .expect("getting the notes should succeed"); + assert_eq!(leaves.len(), 0, "no new leaves should have been created"); + + let nullifier = vec![nullifier]; + let existing_nullifers = existing_nullifiers(session, &nullifier) + .expect("Querrying the nullifiers should work"); + assert!( + existing_nullifers.is_empty(), + "the note shouldn't have been nullified" + ); +} + /// Attempts to convert moonlight DUSK into phoenix DUSK but fails due to not /// targeting the correct contract for the conversion. #[test] diff --git a/contracts/transfer/tests/phoenix.rs b/contracts/transfer/tests/phoenix.rs index ccce9fcddf..38e85e5260 100644 --- a/contracts/transfer/tests/phoenix.rs +++ b/contracts/transfer/tests/phoenix.rs @@ -9,8 +9,9 @@ use std::sync::mpsc; pub mod common; use crate::common::utils::{ - account, chain_id, contract_balance, execute, filter_notes_owned_by, - leaves_from_height, new_owned_notes_value, owned_notes_value, update_root, + account, chain_id, contract_balance, execute, existing_nullifiers, + filter_notes_owned_by, leaves_from_height, new_owned_notes_value, + owned_notes_value, update_root, }; use dusk_bytes::Serializable; @@ -965,6 +966,142 @@ fn contract_withdraw() { ); } +/// Converting moonlight DUSK into phoenix DUSK with a phoenix transaction +/// should fail. +#[test] +fn convert_to_phoenix_fails() { + const CONVERSION_VALUE: u64 = dusk(10.0); + + let rng = &mut StdRng::seed_from_u64(0xfeeb); + + let phoenix_sender_sk = PhoenixSecretKey::random(rng); + let phoenix_sender_vk = PhoenixViewKey::from(&phoenix_sender_sk); + let phoenix_sender_pk = PhoenixPublicKey::from(&phoenix_sender_sk); + + let phoenix_change_pk = phoenix_sender_pk.clone(); + + let moonlight_sk = AccountSecretKey::random(rng); + let moonlight_pk = AccountPublicKey::from(&moonlight_sk); + + let mut session = &mut instantiate::<1>(rng, &phoenix_sender_sk); + + // Add the conversion value to the moonlight account + session + .call::<_, ()>( + TRANSFER_CONTRACT, + "add_account_balance", + &(moonlight_pk, CONVERSION_VALUE), + GAS_LIMIT, + ) + .expect("Inserting genesis account should succeed"); + + // make sure the moonlight account owns the conversion value + let moonlight_account = account(&mut session, &moonlight_pk) + .expect("Getting account should succeed"); + assert_eq!( + moonlight_account.balance, CONVERSION_VALUE, + "The moonlight account should own the conversion value before the transaction" + ); + + // we need to retrieve the genesis-note to generate its nullifier + let leaves = leaves_from_height(session, 0) + .expect("getting the notes should succeed"); + let notes = filter_notes_owned_by( + phoenix_sender_vk, + leaves.into_iter().map(|leaf| leaf.note), + ); + assert_eq!(notes.len(), 1, "There should be one note at this height"); + + // generate a new note stealth-address and note-sk for the conversion + let address = + phoenix_sender_pk.gen_stealth_address(&JubJubScalar::random(&mut *rng)); + let note_sk = phoenix_sender_sk.gen_note_sk(&address); + + // the moonlight replay token + let nonce = 1; + + // a conversion is a deposit into the transfer-contract paired with a + // withdrawal + let contract_call = ContractCall { + contract: TRANSFER_CONTRACT, + fn_name: String::from("convert"), + fn_args: rkyv::to_bytes::<_, 1024>(&Withdraw::new( + rng, + ¬e_sk, + TRANSFER_CONTRACT, + // set the conversion-value as a withdrawal + CONVERSION_VALUE, + WithdrawReceiver::Phoenix(address), + WithdrawReplayToken::Moonlight(nonce), + )) + .expect("should serialize conversion correctly") + .to_vec(), + }; + + let tx = create_phoenix_transaction( + rng, + session, + &phoenix_sender_sk, + &phoenix_change_pk, + &phoenix_sender_pk, + GAS_LIMIT, + LUX, + [0], + 0, + false, + // set the conversion-value as the deposit + CONVERSION_VALUE, + Some(contract_call), + ); + + let receipt = execute(session, tx).expect("Executing TX should succeed"); + + // check that the transaction execution panicked with the correct message + assert!(receipt.data.is_err()); + assert_eq!( + format!("{}", receipt.data.unwrap_err()), + String::from("Panic: Expected Moonlight TX, found Phoenix"), + "The attempted conversion from moonlight to phoenix when paying gas with phoenix should error" + ); + assert_eq!( + receipt.gas_spent, + GAS_LIMIT * LUX, + "The max gas should have been spent" + ); + + update_root(session).expect("Updating the root should succeed"); + + println!("CONVERT TO PHOENIX: {} gas", receipt.gas_spent); + + let moonlight_account = account(&mut session, &moonlight_pk) + .expect("Getting account should succeed"); + + assert_eq!( + moonlight_account.balance, + CONVERSION_VALUE, + "Since the conversion failed, the moonlight account should still own the conversion value" + ); + + let leaves = leaves_from_height(session, 1) + .expect("getting the notes should succeed"); + let notes = filter_notes_owned_by( + phoenix_sender_vk, + leaves.into_iter().map(|leaf| leaf.note), + ); + let notes_value = owned_notes_value(phoenix_sender_vk, ¬es); + + assert_eq!( + notes.len(), + 2, + "Change and refund notes should have been created" + ); + assert_eq!( + notes_value, + PHOENIX_GENESIS_VALUE - receipt.gas_spent, + "The new notes should have the original value minus the spent gas" + ); +} + /// Convert phoenix DUSK into moonlight DUSK. #[test] fn convert_to_moonlight() { @@ -1414,20 +1551,6 @@ fn opening( .map(|r| r.data) } -fn existing_nullifiers( - session: &mut Session, - nullifiers: &Vec, -) -> Result, PiecrustError> { - session - .call( - TRANSFER_CONTRACT, - "existing_nullifiers", - &nullifiers.clone(), - GAS_LIMIT, - ) - .map(|r| r.data) -} - fn gen_nullifiers( session: &mut Session, notes_pos: impl AsRef<[u64]>,