diff --git a/prdoc/pr_10554.prdoc b/prdoc/pr_10554.prdoc new file mode 100644 index 0000000000000..5298a312dd691 --- /dev/null +++ b/prdoc/pr_10554.prdoc @@ -0,0 +1,14 @@ +title: '[pallet-revive] add EVM gas call syscalls' +doc: +- audience: Runtime Dev + description: |- + This PR adds two new syscalls for calls accepting EVM gas instead of Weight and Deposit. + + This is an important change for the initial release as it will align PVM contracts closer to EVM (the problem can't be solved in the Solidity compiler). +crates: +- name: pallet-revive-fixtures + bump: minor +- name: pallet-revive + bump: minor +- name: pallet-revive-uapi + bump: minor diff --git a/substrate/frame/revive/fixtures/contracts/call_with_gas.rs b/substrate/frame/revive/fixtures/contracts/call_with_gas.rs new file mode 100644 index 0000000000000..10d918c9ed06d --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/call_with_gas.rs @@ -0,0 +1,42 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +#![no_main] + +include!("../panic_handler.rs"); + +use uapi::{input, CallFlags, HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 256, + callee_addr: &[u8; 20], + gas: u64, + ); + + let mut value = [0; 32]; + api::value_transferred(&mut value); + + api::call_evm(CallFlags::empty(), callee_addr, gas, &value, &[], None).unwrap(); +} diff --git a/substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs b/substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs new file mode 100644 index 0000000000000..cffc33efb6b99 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs @@ -0,0 +1,52 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +#![no_main] +include!("../panic_handler.rs"); + +use uapi::{input, CallFlags, HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + address: &[u8; 20], + gas: u64, + ); + + let mut key = [0u8; 32]; + key[0] = 1u8; + + let mut value = [0u8; 32]; + let value = &mut &mut value[..]; + value[0] = 2u8; + + api::set_storage(StorageFlags::empty(), &key, value); + api::get_storage(StorageFlags::empty(), &key, value).unwrap(); + assert!(value[0] == 2u8); + + api::delegate_call_evm(CallFlags::empty(), address, gas, &[], None).unwrap(); + + api::get_storage(StorageFlags::empty(), &key, value).unwrap(); + assert!(value[0] == 1u8); +} + diff --git a/substrate/frame/revive/src/tests/pvm.rs b/substrate/frame/revive/src/tests/pvm.rs index dbeffcab840fb..787a0824e0a81 100644 --- a/substrate/frame/revive/src/tests/pvm.rs +++ b/substrate/frame/revive/src/tests/pvm.rs @@ -5536,3 +5536,78 @@ fn self_destruct_by_syscall_tracing_works() { }); } } + +#[test] +fn delegate_call_with_gas_limit() { + let (caller_binary, _caller_code_hash) = compile_module("delegate_call_evm").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("delegate_call_lib").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // fails, not enough gas + assert_err!( + builder::bare_call(caller_addr) + .native_value(1337) + .data((callee_addr, 100u64).encode()) + .build() + .result, + Error::::ContractTrapped, + ); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, 100_000_000_000u64).encode()) + .build()); + }); +} + +#[test] +fn call_with_gas_limit() { + let (caller_binary, _caller_code_hash) = compile_module("call_with_gas").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // fails, not enough gas + assert_err!( + builder::bare_call(caller_addr) + .data((callee_addr, 1u64).encode()) + .build() + .result, + Error::::ContractTrapped, + ); + + // succeeds, not enough gas but call stipend will be added + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, 100u64).encode()) + .build()); + + // succeeds, enough gas + assert_ok!(builder::call(caller_addr) + .data((callee_addr, 100_000_000_000u64).encode()) + .build()); + }); +} diff --git a/substrate/frame/revive/src/vm/pvm.rs b/substrate/frame/revive/src/vm/pvm.rs index 73a6c712ed3b2..e582cc5c95475 100644 --- a/substrate/frame/revive/src/vm/pvm.rs +++ b/substrate/frame/revive/src/vm/pvm.rs @@ -631,8 +631,7 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { flags: CallFlags, call_type: CallType, callee_ptr: u32, - deposit_ptr: u32, - weight: Weight, + resources: &CallResources, input_data_ptr: u32, input_data_len: u32, output_ptr: u32, @@ -647,8 +646,6 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { None => self.charge_gas(call_type.cost())?, }; - let deposit_limit = memory.read_u256(deposit_ptr)?; - // we do check this in exec.rs but we want to error out early if input_data_len > limits::CALLDATA_BYTES { Err(>::CallDataTooLarge)?; @@ -693,24 +690,13 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { ReentrancyProtection::Strict }; - self.ext.call( - &CallResources::from_weight_and_deposit(weight, deposit_limit), - &callee, - value, - input_data, - reentrancy, - read_only, - ) + self.ext.call(resources, &callee, value, input_data, reentrancy, read_only) }, CallType::DelegateCall => { if flags.intersects(CallFlags::ALLOW_REENTRY | CallFlags::READ_ONLY) { return Err(Error::::InvalidCallFlags.into()); } - self.ext.delegate_call( - &CallResources::from_weight_and_deposit(weight, deposit_limit), - callee, - input_data, - ) + self.ext.delegate_call(resources, callee, input_data) }, }; diff --git a/substrate/frame/revive/src/vm/pvm/env.rs b/substrate/frame/revive/src/vm/pvm/env.rs index a8caac3848922..5232c45497160 100644 --- a/substrate/frame/revive/src/vm/pvm/env.rs +++ b/substrate/frame/revive/src/vm/pvm/env.rs @@ -304,14 +304,55 @@ pub mod env { let (deposit_ptr, value_ptr) = extract_hi_lo(deposit_and_value); let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let weight = Weight::from_parts(ref_time_limit, proof_size_limit); + + self.charge_gas(RuntimeCosts::CopyFromContract(32))?; + let deposit_limit = memory.read_u256(deposit_ptr)?; self.call( memory, CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, CallType::Call { value_ptr }, callee_ptr, - deposit_ptr, - Weight::from_parts(ref_time_limit, proof_size_limit), + &CallResources::from_weight_and_deposit(weight, deposit_limit), + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Make a call to another contract. + /// See [`pallet_revive_uapi::HostFn::call_evm`]. + #[stable] + fn call_evm( + &mut self, + memory: &mut M, + flags: u32, + callee: u32, + value_ptr: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> Result { + let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let resources = if gas == u64::MAX { + CallResources::NoLimits + } else { + self.charge_gas(RuntimeCosts::CopyFromContract(32))?; + let value = memory.read_u256(value_ptr)?; + // We also need to detect the 2300: We need to add something scaled. + let add_stipend = !value.is_zero() || gas == revm::interpreter::gas::CALL_STIPEND; + CallResources::from_ethereum_gas(gas.into(), add_stipend) + }; + + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::Call { value_ptr }, + callee, + &resources, input_data_ptr, input_data_len, output_ptr, @@ -335,14 +376,50 @@ pub mod env { let (flags, address_ptr) = extract_hi_lo(flags_and_callee); let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let weight = Weight::from_parts(ref_time_limit, proof_size_limit); + + self.charge_gas(RuntimeCosts::CopyFromContract(32))?; + let deposit_limit = memory.read_u256(deposit_ptr)?; self.call( memory, CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, CallType::DelegateCall, address_ptr, - deposit_ptr, - Weight::from_parts(ref_time_limit, proof_size_limit), + &CallResources::from_weight_and_deposit(weight, deposit_limit), + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Same as `delegate_call` but with EVM gas. + /// See [`pallet_revive_uapi::HostFn::delegate_call_evm`]. + #[stable] + fn delegate_call_evm( + &mut self, + memory: &mut M, + flags: u32, + callee: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> Result { + let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let resources = if gas == u64::MAX { + CallResources::NoLimits + } else { + CallResources::from_ethereum_gas(gas.into(), false) + }; + + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::DelegateCall, + callee, + &resources, input_data_ptr, input_data_len, output_ptr, diff --git a/substrate/frame/revive/uapi/src/host.rs b/substrate/frame/revive/uapi/src/host.rs index c85d175237a6f..65fe94aa587c0 100644 --- a/substrate/frame/revive/uapi/src/host.rs +++ b/substrate/frame/revive/uapi/src/host.rs @@ -119,6 +119,20 @@ pub trait HostFn: private::Sealed { output: Option<&mut &mut [u8]>, ) -> Result; + /// Same as [HostFn::call] but receives the one-dimensional EVM gas argument. + /// + /// Adds the EVM gas stipend for non-zero value calls. + /// + /// If gas is `u64::MAX`, the call will run with uncapped limits. + fn call_evm( + flags: CallFlags, + callee: &[u8; 20], + gas: u64, + value: &[u8; 32], + input_data: &[u8], + output: Option<&mut &mut [u8]>, + ) -> Result; + /// Stores the address of the caller into the supplied buffer. /// /// If this is a top-level call (i.e. initiated by an extrinsic) the origin address of the @@ -207,6 +221,17 @@ pub trait HostFn: private::Sealed { output: Option<&mut &mut [u8]>, ) -> Result; + /// Same as [HostFn::delegate_call] but receives the one-dimensional EVM gas argument. + /// + /// If gas is `u64::MAX`, the call will run with uncapped limits. + fn delegate_call_evm( + flags: CallFlags, + address: &[u8; 20], + gas: u64, + input_data: &[u8], + output: Option<&mut &mut [u8]>, + ) -> Result; + /// Deposit a contract event with the data buffer and optional list of topics. There is a limit /// on the maximum number of topics specified by `event_topics`. /// diff --git a/substrate/frame/revive/uapi/src/host/riscv64.rs b/substrate/frame/revive/uapi/src/host/riscv64.rs index fdfd234d7de5e..2e87404a7b558 100644 --- a/substrate/frame/revive/uapi/src/host/riscv64.rs +++ b/substrate/frame/revive/uapi/src/host/riscv64.rs @@ -64,6 +64,14 @@ mod sys { input_data: u64, output_data: u64, ) -> ReturnCode; + pub fn call_evm( + flags: u32, + callee: u32, + value_ptr: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> ReturnCode; pub fn delegate_call( flags_and_callee: u64, ref_time_limit: u64, @@ -80,6 +88,13 @@ mod sys { output_data: u64, address_and_salt: u64, ) -> ReturnCode; + pub fn delegate_call_evm( + flags: u32, + callee: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> ReturnCode; pub fn terminate(beneficiary_ptr: *const u8); pub fn call_data_copy(out_ptr: *mut u8, out_len: u32, offset: u32); pub fn call_data_load(out_ptr: *mut u8, offset: u32); @@ -227,6 +242,36 @@ impl HostFn for HostFnImpl { ret_code.into() } + fn call_evm( + flags: CallFlags, + callee: &[u8; 20], + gas: u64, + value_ptr: &[u8; 32], + input: &[u8], + mut output: Option<&mut &mut [u8]>, + ) -> Result { + let input_data = pack_hi_lo(input.len() as _, input.as_ptr() as _); + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let output_data = pack_hi_lo(&mut output_len as *mut _ as _, output_ptr as _); + + let ret_code = unsafe { + sys::call_evm( + flags.bits(), + callee.as_ptr() as _, + value_ptr.as_ptr() as _, + gas, + input_data, + output_data, + ) + }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + fn delegate_call( flags: CallFlags, address: &[u8; 20], @@ -261,6 +306,34 @@ impl HostFn for HostFnImpl { ret_code.into() } + fn delegate_call_evm( + flags: CallFlags, + address: &[u8; 20], + gas: u64, + input: &[u8], + mut output: Option<&mut &mut [u8]>, + ) -> Result { + let input_data = pack_hi_lo(input.len() as u32, input.as_ptr() as u32); + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let output_data = pack_hi_lo(&mut output_len as *mut _ as u32, output_ptr as u32); + + let ret_code = unsafe { + sys::delegate_call_evm( + flags.bits(), + address.as_ptr() as _, + gas, + input_data, + output_data, + ) + }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + fn deposit_event(topics: &[[u8; 32]], data: &[u8]) { unsafe { sys::deposit_event(