Skip to content

Commit

Permalink
Merge pull request #2970 from dusk-network/mocello/convert_gas_payment
Browse files Browse the repository at this point in the history
transfer-contract: Add tests for failing conversions
  • Loading branch information
moCello authored Nov 14, 2024
2 parents fbc671f + 4f2fcb7 commit ae6d35c
Show file tree
Hide file tree
Showing 3 changed files with 294 additions and 28 deletions.
28 changes: 20 additions & 8 deletions contracts/transfer/tests/common/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -141,3 +139,17 @@ pub fn filter_notes_owned_by<I: IntoIterator<Item = Note>>(
.filter(|note| vk.owns(note.stealth_address()))
.collect()
}

pub fn existing_nullifiers(
session: &mut Session,
nullifiers: &Vec<BlsScalar>,
) -> Result<Vec<BlsScalar>, PiecrustError> {
session
.call(
TRANSFER_CONTRACT,
"existing_nullifiers",
&nullifiers.clone(),
GAS_LIMIT,
)
.map(|r| r.data)
}
139 changes: 135 additions & 4 deletions contracts/transfer/tests/moonlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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};

Expand Down Expand Up @@ -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]
Expand Down
155 changes: 139 additions & 16 deletions contracts/transfer/tests/phoenix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
&note_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, &notes);

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() {
Expand Down Expand Up @@ -1414,20 +1551,6 @@ fn opening(
.map(|r| r.data)
}

fn existing_nullifiers(
session: &mut Session,
nullifiers: &Vec<BlsScalar>,
) -> Result<Vec<BlsScalar>, 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]>,
Expand Down

0 comments on commit ae6d35c

Please sign in to comment.