Skip to content

Commit

Permalink
Merge #5045
Browse files Browse the repository at this point in the history
5045: Custom Payment QoL r=EdHastingsCasperAssociation a=EdHastingsCasperAssociation

This PR adds a guardrail to improve quality of life for custom payment use case.


Co-authored-by: Ed Hastings <[email protected]>
Co-authored-by: Michał Papierski <[email protected]>
Co-authored-by: edhastings <[email protected]>
  • Loading branch information
3 people authored Dec 20, 2024
2 parents 0793d68 + 42b366c commit 10feaad
Show file tree
Hide file tree
Showing 10 changed files with 364 additions and 18 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 52 additions & 7 deletions execution_engine/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::{

use casper_wasm::elements::Module;
use casper_wasmi::{MemoryRef, Trap, TrapCode};
use tracing::{debug, error};
use tracing::{debug, error, warn};

#[cfg(feature = "test-support")]
use casper_wasmi::RuntimeValue;
Expand Down Expand Up @@ -372,6 +372,15 @@ where
) -> Result<(), Trap> {
let name = self.string_from_mem(name_ptr, name_size)?;
let key = self.key_from_mem(key_ptr, key_size)?;

if let Some(payment_purse) = self.context.maybe_payment_purse() {
if Key::URef(payment_purse).normalize() == key.normalize() {
warn!("attempt to put_key payment purse");
return Err(Into::into(ExecError::Revert(ApiError::HandlePayment(
handle_payment::Error::AttemptToPersistPaymentPurse as u8,
))));
}
}
self.context.put_key(name, key).map_err(Into::into)
}

Expand Down Expand Up @@ -920,13 +929,17 @@ where
let handle_payment_costs = system_config.handle_payment_costs();

let result = match entry_point_name {
handle_payment::METHOD_GET_PAYMENT_PURSE => (|| {
handle_payment::METHOD_GET_PAYMENT_PURSE => {
runtime.charge_system_contract_call(handle_payment_costs.get_payment_purse)?;

let rights_controlled_purse =
runtime.get_payment_purse().map_err(Self::reverter)?;
CLValue::from_t(rights_controlled_purse).map_err(Self::reverter)
})(),
match self.context.maybe_payment_purse() {
Some(payment_purse) => CLValue::from_t(payment_purse).map_err(Self::reverter),
None => {
let payment_purse = runtime.get_payment_purse().map_err(Self::reverter)?;
self.context.set_payment_purse(payment_purse);
CLValue::from_t(payment_purse).map_err(Self::reverter)
}
}
}
handle_payment::METHOD_SET_REFUND_PURSE => (|| {
runtime.charge_system_contract_call(handle_payment_costs.set_refund_purse)?;

Expand Down Expand Up @@ -2017,6 +2030,22 @@ where
return Ok(Err(err));
}
let args: RuntimeArgs = bytesrepr::deserialize_from_slice(args_bytes)?;

if let Some(payment_purse) = self.context.maybe_payment_purse() {
for named_arg in args.named_args() {
if utils::extract_urefs(named_arg.cl_value())?
.into_iter()
.any(|uref| uref.remove_access_rights() == payment_purse.remove_access_rights())
{
warn!("attempt to call_contract with payment purse");

return Err(Into::into(ExecError::Revert(ApiError::HandlePayment(
handle_payment::Error::AttemptToPersistPaymentPurse as u8,
))));
}
}
}

let result = self.call_contract(contract_hash, entry_point_name, args)?;
self.manage_call_contract_host_buffer(result_size_ptr, result)
}
Expand All @@ -2034,6 +2063,22 @@ where
return Ok(Err(err));
}
let args: RuntimeArgs = bytesrepr::deserialize_from_slice(args_bytes)?;

if let Some(payment_purse) = self.context.maybe_payment_purse() {
for named_arg in args.named_args() {
if utils::extract_urefs(named_arg.cl_value())?
.into_iter()
.any(|uref| uref.remove_access_rights() == payment_purse.remove_access_rights())
{
warn!("attempt to call_versioned_contract with payment purse");

return Err(Into::into(ExecError::Revert(ApiError::HandlePayment(
handle_payment::Error::AttemptToPersistPaymentPurse as u8,
))));
}
}
}

let result = self.call_versioned_contract(
contract_package_hash,
contract_version,
Expand Down
2 changes: 1 addition & 1 deletion execution_engine/src/runtime/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub(super) fn attenuate_uref_in_args(

/// Extracts a copy of every uref able to be deserialized from `cl_value`.
pub(super) fn extract_urefs(cl_value: &CLValue) -> Result<Vec<URef>, ExecError> {
let mut vec: Vec<URef> = Vec::new();
let mut vec: Vec<URef> = Default::default();
rewrite_urefs(cl_value.clone(), |uref| {
vec.push(*uref);
})?;
Expand Down
14 changes: 14 additions & 0 deletions execution_engine/src/runtime_context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ pub struct RuntimeContext<'a, R> {
account_hash: AccountHash,
emit_message_cost: U512,
allow_install_upgrade: AllowInstallUpgrade,
payment_purse: Option<URef>,
}

impl<'a, R> RuntimeContext<'a, R>
Expand Down Expand Up @@ -149,6 +150,7 @@ where
remaining_spending_limit,
emit_message_cost,
allow_install_upgrade,
payment_purse: None,
}
}

Expand Down Expand Up @@ -180,6 +182,7 @@ where
let remaining_spending_limit = self.remaining_spending_limit();

let transfers = self.transfers.clone();
let payment_purse = self.payment_purse;

RuntimeContext {
tracking_copy,
Expand All @@ -203,6 +206,7 @@ where
remaining_spending_limit,
emit_message_cost: self.emit_message_cost,
allow_install_upgrade: self.allow_install_upgrade,
payment_purse,
}
}

Expand Down Expand Up @@ -231,6 +235,16 @@ where
self.named_keys.contains(name)
}

/// Returns the payment purse, if set.
pub fn maybe_payment_purse(&self) -> Option<URef> {
self.payment_purse
}

/// Sets the payment purse to the imputed uref.
pub fn set_payment_purse(&mut self, uref: URef) {
self.payment_purse = Some(uref);
}

/// Returns an instance of the engine config.
pub fn engine_config(&self) -> &EngineConfig {
&self.engine_config
Expand Down
123 changes: 120 additions & 3 deletions execution_engine_testing/tests/src/test/deploy/non_standard_payment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@ use casper_engine_test_support::{
DEFAULT_PAYMENT, DEFAULT_PROTOCOL_VERSION, LOCAL_GENESIS_REQUEST,
MINIMUM_ACCOUNT_CREATION_BALANCE,
};
use casper_execution_engine::engine_state::BlockInfo;
use casper_execution_engine::{engine_state::BlockInfo, execution::ExecError};
use casper_storage::data_access_layer::BalanceIdentifier;
use casper_types::{
account::AccountHash, runtime_args, BlockHash, Digest, Gas, RuntimeArgs, Timestamp, U512,
account::AccountHash, runtime_args, ApiError, BlockHash, Digest, Gas, RuntimeArgs, Timestamp,
U512,
};

const ACCOUNT_1_ADDR: AccountHash = AccountHash::new([42u8; 32]);
const DO_NOTHING_WASM: &str = "do_nothing.wasm";
const CONTRACT_TRANSFER_TO_ACCOUNT: &str = "transfer_to_account_u512.wasm";
const TRANSFER_MAIN_PURSE_TO_NEW_PURSE_WASM: &str = "transfer_main_purse_to_new_purse.wasm";
const PAYMENT_PURSE_PERSIST_WASM: &str = "payment_purse_persist.wasm";
const NAMED_PURSE_PAYMENT_WASM: &str = "named_purse_payment.wasm";
const ARG_TARGET: &str = "target";
const ARG_AMOUNT: &str = "amount";
const ARG_PURSE_NAME: &str = "purse_name";
const ARG_DESTINATION: &str = "destination";

#[ignore]
#[allow(unused)]
#[test]
fn should_charge_non_main_purse() {
// as account_1, create & fund a new purse and use that to pay for something
Expand Down Expand Up @@ -126,3 +127,119 @@ fn should_charge_non_main_purse() {
"since we zero'd out the paying purse, the final balance should be zero"
);
}

const ARG_METHOD: &str = "method";

#[ignore]
#[test]
fn should_not_allow_custom_payment_purse_persistence_1() {
let mut builder = LmdbWasmTestBuilder::default();

builder.run_genesis(LOCAL_GENESIS_REQUEST.clone());

let account_hash = *DEFAULT_ACCOUNT_ADDR;

let deploy_item = DeployItemBuilder::new()
.with_address(account_hash)
.with_session_code(DO_NOTHING_WASM, RuntimeArgs::default())
.with_payment_code(
PAYMENT_PURSE_PERSIST_WASM,
runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT, ARG_METHOD => "put_key"},
)
.with_deploy_hash([1; 32])
.with_authorization_keys(&[account_hash])
.build();
let block_info = BlockInfo::new(
Digest::default(),
Timestamp::now().millis().into(),
BlockHash::default(),
1,
);
let limit = Gas::from(12_500_000_000_u64);

let request = deploy_item
.new_custom_payment_from_deploy_item(block_info, limit)
.expect("should be valid req");

builder.exec_wasm_v1(request).expect_failure();

builder.assert_error(casper_execution_engine::engine_state::Error::Exec(
ExecError::Revert(ApiError::HandlePayment(40)),
));
}

#[ignore]
#[test]
fn should_not_allow_custom_payment_purse_persistence_2() {
let mut builder = LmdbWasmTestBuilder::default();

builder.run_genesis(LOCAL_GENESIS_REQUEST.clone());

let account_hash = *DEFAULT_ACCOUNT_ADDR;

let deploy_item = DeployItemBuilder::new()
.with_address(account_hash)
.with_session_code(DO_NOTHING_WASM, RuntimeArgs::default())
.with_payment_code(
PAYMENT_PURSE_PERSIST_WASM,
runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT, ARG_METHOD => "call_contract"},
)
.with_deploy_hash([1; 32])
.with_authorization_keys(&[account_hash])
.build();
let block_info = BlockInfo::new(
Digest::default(),
Timestamp::now().millis().into(),
BlockHash::default(),
1,
);
let limit = Gas::from(12_500_000_000_u64);

let request = deploy_item
.new_custom_payment_from_deploy_item(block_info, limit)
.expect("should be valid req");

builder.exec_wasm_v1(request).expect_failure();

builder.assert_error(casper_execution_engine::engine_state::Error::Exec(
ExecError::Revert(ApiError::HandlePayment(40)),
));
}

#[ignore]
#[test]
fn should_not_allow_custom_payment_purse_persistence_3() {
let mut builder = LmdbWasmTestBuilder::default();

builder.run_genesis(LOCAL_GENESIS_REQUEST.clone());

let account_hash = *DEFAULT_ACCOUNT_ADDR;

let deploy_item = DeployItemBuilder::new()
.with_address(account_hash)
.with_session_code(DO_NOTHING_WASM, RuntimeArgs::default())
.with_payment_code(
PAYMENT_PURSE_PERSIST_WASM,
runtime_args! {ARG_AMOUNT => *DEFAULT_PAYMENT, ARG_METHOD => "call_versioned_contract"},
)
.with_deploy_hash([1; 32])
.with_authorization_keys(&[account_hash])
.build();
let block_info = BlockInfo::new(
Digest::default(),
Timestamp::now().millis().into(),
BlockHash::default(),
1,
);
let limit = Gas::from(12_500_000_000_u64);

let request = deploy_item
.new_custom_payment_from_deploy_item(block_info, limit)
.expect("should be valid req");

builder.exec_wasm_v1(request).expect_failure();

builder.assert_error(casper_execution_engine::engine_state::Error::Exec(
ExecError::Revert(ApiError::HandlePayment(40)),
));
}
16 changes: 16 additions & 0 deletions smart_contracts/contracts/test/payment-purse-persist/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "payment-purse-persist"
version = "0.1.0"
authors = ["Ed Hastings <[email protected]>", "Michał Papierski <[email protected]>"]
edition = "2021"

[[bin]]
name = "payment_purse_persist"
path = "src/main.rs"
bench = false
doctest = false
test = false

[dependencies]
casper-contract = { path = "../../../contract" }
casper-types = { path = "../../../../types" }
63 changes: 63 additions & 0 deletions smart_contracts/contracts/test/payment-purse-persist/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#![no_std]
#![no_main]

extern crate alloc;

use alloc::string::String;
use casper_contract::contract_api::{runtime, runtime::put_key, system};
use casper_types::{contracts::ContractPackageHash, runtime_args, ApiError, RuntimeArgs, URef};

const GET_PAYMENT_PURSE: &str = "get_payment_purse";
const THIS_SHOULD_FAIL: &str = "this_should_fail";

const ARG_METHOD: &str = "method";

/// This logic is intended to be used as SESSION PAYMENT LOGIC
/// It gets the payment purse and attempts and attempts to persist it,
/// which should fail.
#[no_mangle]
pub extern "C" fn call() {
let method: String = runtime::get_named_arg(ARG_METHOD);

// handle payment contract
let handle_payment_contract_hash = system::get_handle_payment();

// get payment purse for current execution
let payment_purse: URef = runtime::call_contract(
handle_payment_contract_hash,
GET_PAYMENT_PURSE,
RuntimeArgs::default(),
);

if method == "put_key" {
// attempt to persist the payment purse, which should fail
put_key(THIS_SHOULD_FAIL, payment_purse.into());
} else if method == "call_contract" {
// attempt to call a contract with the payment purse, which should fail
let _payment_purse: URef = runtime::call_contract(
handle_payment_contract_hash,
GET_PAYMENT_PURSE,
runtime_args! {
"payment_purse" => payment_purse,
},
);

// should never reach here
runtime::revert(ApiError::User(1000));
} else if method == "call_versioned_contract" {
// attempt to call a versioned contract with the payment purse, which should fail
let _payment_purse: URef = runtime::call_versioned_contract(
ContractPackageHash::new(handle_payment_contract_hash.value()),
None, // Latest
GET_PAYMENT_PURSE,
runtime_args! {
"payment_purse" => payment_purse,
},
);

// should never reach here
runtime::revert(ApiError::User(1001));
} else {
runtime::revert(ApiError::User(2000));
}
}
Loading

0 comments on commit 10feaad

Please sign in to comment.