Skip to content

Commit

Permalink
Cross-VM payable calls & XVM tests (#988)
Browse files Browse the repository at this point in the history
* Add value param to 'XvmCall'.

* Fix precompile tests.

* Update types in CE & precompiles.

* Add EVM call tests for XVM.

* New type idiom for EthereumTxInput.

* Add XVM integration tests.

* More XVM integration tests.

* Fix runtime tests.

* Move contract deploy helpers to setup.rs.

* Update value adjustment for wasm payable calls.

* More pallet-xvm unit tests.

* Update pallet-xvm weight info.

* Apply review suggestions.

* Fix runtime tests.

* Calling WASM payable from EVM fails if caller contract balance below ED.

* Update weight info.

* Fix unit tests.
  • Loading branch information
shaunxw authored Aug 6, 2023
1 parent 79d4568 commit ad59483
Show file tree
Hide file tree
Showing 28 changed files with 1,514 additions and 91 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.

7 changes: 4 additions & 3 deletions chain-extensions/types/xvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

#![cfg_attr(not(feature = "std"), no_std)]

use astar_primitives::xvm::CallError;
use astar_primitives::{xvm::CallError, Balance};
use parity_scale_codec::{Decode, Encode};
use sp_std::vec::Vec;

Expand All @@ -41,8 +41,7 @@ impl From<CallError> for XvmExecutionResult {
SameVmCallNotAllowed => 2,
InvalidTarget => 3,
InputTooLarge => 4,
BadOrigin => 5,
ExecutionFailed(_) => 6,
ExecutionFailed(_) => 5,
};
Self::Err(error_code)
}
Expand All @@ -65,4 +64,6 @@ pub struct XvmCallArgs {
pub to: Vec<u8>,
/// Encoded call params
pub input: Vec<u8>,
/// Value to transfer
pub value: Balance,
}
36 changes: 13 additions & 23 deletions chain-extensions/xvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,15 @@

#![cfg_attr(not(feature = "std"), no_std)]

use astar_primitives::xvm::{CallError, Context, VmId, XvmCall};
use astar_primitives::xvm::{Context, VmId, XvmCall};
use frame_support::dispatch::Encode;
use pallet_contracts::{
chain_extension::{ChainExtension, Environment, Ext, InitState, RetVal},
Origin,
};
use pallet_contracts::chain_extension::{ChainExtension, Environment, Ext, InitState, RetVal};
use sp_runtime::DispatchError;
use sp_std::marker::PhantomData;
use xvm_chain_extension_types::{XvmCallArgs, XvmExecutionResult};

enum XvmFuncId {
Call,
// TODO: expand with other calls too
}

impl TryFrom<u16> for XvmFuncId {
Expand Down Expand Up @@ -76,28 +72,22 @@ where
// So we will charge a 32KB dummy value as a temporary replacement.
let charged_weight = env.charge_weight(weight_limit.set_proof_size(32 * 1024))?;

let caller = match env.ext().caller().clone() {
Origin::Signed(address) => address,
Origin::Root => {
log::trace!(
target: "xvm-extension::xvm_call",
"root origin not supported"
);
return Ok(RetVal::Converging(
XvmExecutionResult::from(CallError::BadOrigin).into(),
));
}
};
let XvmCallArgs {
vm_id,
to,
input,
value,
} = env.read_as_unbounded(env.in_len())?;

let XvmCallArgs { vm_id, to, input } = env.read_as_unbounded(env.in_len())?;
// Similar to EVM behavior, the `source` should be (limited to) the
// contract address. Otherwise contracts would be able to do arbitrary
// things on behalf of the caller via XVM.
let source = env.ext().address();

let _origin_address = env.ext().address().clone();
let _value = env.ext().value_transferred();
let xvm_context = Context {
source_vm_id: VmId::Wasm,
weight_limit,
};

let vm_id = {
match TryInto::<VmId>::try_into(vm_id) {
Ok(id) => id,
Expand All @@ -108,7 +98,7 @@ where
}
}
};
let call_result = XC::call(xvm_context, vm_id, caller, to, input);
let call_result = XC::call(xvm_context, vm_id, source.clone(), to, input, value);

let actual_weight = match call_result {
Ok(ref info) => info.used_weight,
Expand Down
4 changes: 2 additions & 2 deletions pallets/ethereum-checked/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

use super::*;

use astar_primitives::ethereum_checked::MAX_ETHEREUM_TX_INPUT_SIZE;
use astar_primitives::ethereum_checked::EthereumTxInput;
use frame_benchmarking::v2::*;

#[benchmarks]
Expand All @@ -31,7 +31,7 @@ mod benchmarks {
let target =
H160::from_slice(&hex::decode("dfb975d018f03994a3b943808e3aa0964bd78463").unwrap());
// Calling `store(3)`
let input = BoundedVec::<u8, ConstU32<MAX_ETHEREUM_TX_INPUT_SIZE>>::try_from(
let input = EthereumTxInput::try_from(
hex::decode("6057361d0000000000000000000000000000000000000000000000000000000000000003")
.unwrap(),
)
Expand Down
2 changes: 1 addition & 1 deletion pallets/ethereum-checked/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ impl ExtBuilder {
assert_ok!(Evm::create2(
RuntimeOrigin::root(),
ALICE_H160,
hex::decode(STORAGE_CONTRACT).unwrap(),
hex::decode(STORAGE_CONTRACT).expect("invalid code hex"),
H256::zero(),
U256::zero(),
1_000_000,
Expand Down
10 changes: 5 additions & 5 deletions pallets/ethereum-checked/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@
use super::*;
use mock::*;

use astar_primitives::ethereum_checked::MAX_ETHEREUM_TX_INPUT_SIZE;
use astar_primitives::ethereum_checked::EthereumTxInput;
use ethereum::{ReceiptV3, TransactionV2 as Transaction};
use frame_support::{assert_noop, assert_ok, traits::ConstU32};
use frame_support::{assert_noop, assert_ok};
use sp_runtime::DispatchError;

fn bounded_input(data: &'static str) -> BoundedVec<u8, ConstU32<MAX_ETHEREUM_TX_INPUT_SIZE>> {
BoundedVec::<u8, ConstU32<MAX_ETHEREUM_TX_INPUT_SIZE>>::try_from(hex::decode(data).unwrap())
.unwrap()
fn bounded_input(data: &'static str) -> EthereumTxInput {
EthereumTxInput::try_from(hex::decode(data).expect("invalid input hex"))
.expect("input too large")
}

#[test]
Expand Down
1 change: 1 addition & 0 deletions pallets/xvm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ astar-primitives = { workspace = true }

[dev-dependencies]
fp-evm = { workspace = true }
hex = { workspace = true }
pallet-balances = { workspace = true, features = ["std"] }
pallet-insecure-randomness-collective-flip = { workspace = true, features = ["std"] }
pallet-timestamp = { workspace = true, features = ["std"] }
Expand Down
14 changes: 11 additions & 3 deletions pallets/xvm/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ use parity_scale_codec::Encode;
use sp_core::H160;
use sp_runtime::MultiAddress;

#[benchmarks]
use astar_primitives::Balance;

#[benchmarks(
where <T as pallet_contracts::Config>::Currency: Currency<T::AccountId, Balance = Balance>,
)]
mod benchmarks {
use super::*;

Expand All @@ -38,10 +42,12 @@ mod benchmarks {
let source = whitelisted_caller();
let target = H160::repeat_byte(1).encode();
let input = vec![1, 2, 3];
let value = 1_000_000u128;

#[block]
{
Pallet::<T>::call_without_execution(context, vm_id, source, target, input).unwrap();
Pallet::<T>::call_without_execution(context, vm_id, source, target, input, value)
.unwrap();
}
}

Expand All @@ -55,10 +61,12 @@ mod benchmarks {
let source = whitelisted_caller();
let target = MultiAddress::<T::AccountId, ()>::Id(whitelisted_caller()).encode();
let input = vec![1, 2, 3];
let value = 1_000_000u128;

#[block]
{
Pallet::<T>::call_without_execution(context, vm_id, source, target, input).unwrap();
Pallet::<T>::call_without_execution(context, vm_id, source, target, input, value)
.unwrap();
}
}

Expand Down
67 changes: 45 additions & 22 deletions pallets/xvm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,20 @@

#![cfg_attr(not(feature = "std"), no_std)]

use frame_support::{ensure, traits::ConstU32, BoundedVec};
use frame_support::{ensure, traits::Currency};
use pallet_contracts::{CollectEvents, DebugInfo, Determinism};
use pallet_evm::GasWeightMapping;
use parity_scale_codec::Decode;
use sp_core::U256;
use sp_core::{H160, U256};
use sp_runtime::traits::StaticLookup;
use sp_std::{marker::PhantomData, prelude::*};

use astar_primitives::{
ethereum_checked::{
AccountMapping, CheckedEthereumTransact, CheckedEthereumTx, MAX_ETHEREUM_TX_INPUT_SIZE,
AccountMapping, CheckedEthereumTransact, CheckedEthereumTx, EthereumTxInput,
},
xvm::{CallError, CallErrorWithWeight, CallInfo, CallResult, Context, VmId, XvmCall},
Balance,
};

#[cfg(feature = "runtime-benchmarks")]
Expand All @@ -58,8 +59,8 @@ mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;

#[cfg(test)]
mod mock;
mod tests;

pub use pallet::*;

Expand Down Expand Up @@ -88,25 +89,35 @@ pub mod pallet {
}
}

impl<T: Config> XvmCall<T::AccountId> for Pallet<T> {
impl<T> XvmCall<T::AccountId> for Pallet<T>
where
T: Config,
T::Currency: Currency<T::AccountId, Balance = Balance>,
{
fn call(
context: Context,
vm_id: VmId,
source: T::AccountId,
target: Vec<u8>,
input: Vec<u8>,
value: Balance,
) -> CallResult {
Pallet::<T>::do_call(context, vm_id, source, target, input, false)
Pallet::<T>::do_call(context, vm_id, source, target, input, value, false)
}
}

impl<T: Config> Pallet<T> {
impl<T> Pallet<T>
where
T: Config,
T::Currency: Currency<T::AccountId, Balance = Balance>,
{
fn do_call(
context: Context,
vm_id: VmId,
source: T::AccountId,
target: Vec<u8>,
input: Vec<u8>,
value: Balance,
skip_execution: bool,
) -> CallResult {
ensure!(
Expand All @@ -121,8 +132,12 @@ impl<T: Config> Pallet<T> {
);

match vm_id {
VmId::Evm => Pallet::<T>::evm_call(context, source, target, input, skip_execution),
VmId::Wasm => Pallet::<T>::wasm_call(context, source, target, input, skip_execution),
VmId::Evm => {
Pallet::<T>::evm_call(context, source, target, input, value, skip_execution)
}
VmId::Wasm => {
Pallet::<T>::wasm_call(context, source, target, input, value, skip_execution)
}
}
}

Expand All @@ -131,26 +146,33 @@ impl<T: Config> Pallet<T> {
source: T::AccountId,
target: Vec<u8>,
input: Vec<u8>,
value: Balance,
skip_execution: bool,
) -> CallResult {
log::trace!(
target: "xvm::evm_call",
"Calling EVM: {:?} {:?}, {:?}, {:?}",
context, source, target, input,
"Calling EVM: {:?} {:?}, {:?}, {:?}, {:?}",
context, source, target, input, value,
);

ensure!(
target.len() == H160::len_bytes(),
CallErrorWithWeight {
error: CallError::InvalidTarget,
used_weight: WeightInfoOf::<T>::evm_call_overheads(),
}
);
let target_decoded =
Decode::decode(&mut target.as_ref()).map_err(|_| CallErrorWithWeight {
error: CallError::InvalidTarget,
used_weight: WeightInfoOf::<T>::evm_call_overheads(),
})?;
let bounded_input = BoundedVec::<u8, ConstU32<MAX_ETHEREUM_TX_INPUT_SIZE>>::try_from(input)
.map_err(|_| CallErrorWithWeight {
error: CallError::InputTooLarge,
used_weight: WeightInfoOf::<T>::evm_call_overheads(),
})?;
let bounded_input = EthereumTxInput::try_from(input).map_err(|_| CallErrorWithWeight {
error: CallError::InputTooLarge,
used_weight: WeightInfoOf::<T>::evm_call_overheads(),
})?;

let value = U256::zero();
let value_u256 = U256::from(value);
// With overheads, less weight is available.
let weight_limit = context
.weight_limit
Expand All @@ -161,7 +183,7 @@ impl<T: Config> Pallet<T> {
let tx = CheckedEthereumTx {
gas_limit,
target: target_decoded,
value,
value: value_u256,
input: bounded_input,
maybe_access_list: None,
};
Expand Down Expand Up @@ -210,12 +232,13 @@ impl<T: Config> Pallet<T> {
source: T::AccountId,
target: Vec<u8>,
input: Vec<u8>,
value: Balance,
skip_execution: bool,
) -> CallResult {
log::trace!(
target: "xvm::wasm_call",
"Calling WASM: {:?} {:?}, {:?}, {:?}",
context, source, target, input,
"Calling WASM: {:?} {:?}, {:?}, {:?}, {:?}",
context, source, target, input, value,
);

let dest = {
Expand All @@ -231,7 +254,6 @@ impl<T: Config> Pallet<T> {
let weight_limit = context
.weight_limit
.saturating_sub(WeightInfoOf::<T>::wasm_call_overheads());
let value = Default::default();

// Note the skip execution check should be exactly before `pallet_contracts::bare_call`
// to benchmark the correct overheads.
Expand Down Expand Up @@ -277,7 +299,8 @@ impl<T: Config> Pallet<T> {
source: T::AccountId,
target: Vec<u8>,
input: Vec<u8>,
value: Balance,
) -> CallResult {
Self::do_call(context, vm_id, source, target, input, true)
Self::do_call(context, vm_id, source, target, input, value, true)
}
}
Loading

0 comments on commit ad59483

Please sign in to comment.