use alloc::{vec, vec::Vec};
use core::{fmt, marker::PhantomData};
use fp_evm::PrecompileHandle;
use frame_support::weights::Weight;
use pallet_evm_polkavm_proc_macro::define_env;
use pallet_evm_polkavm_uapi::{ReturnErrorCode, ReturnFlags};
use scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
use sp_core::{H160, H256, U256};
use sp_runtime::RuntimeDebug;
use super::{LOG_TARGET, SENTINEL};
use crate::{Config, ConvertPolkaVmGas, WeightInfo};
#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Default)]
pub struct ExecReturnValue {
pub flags: ReturnFlags,
pub data: Vec<u8>,
}
pub type ExecResult = Result<ExecReturnValue, SupervisorError>;
impl ExecReturnValue {
pub fn did_revert(&self) -> bool {
self.flags.contains(ReturnFlags::REVERT)
}
}
pub trait Memory {
fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), SupervisorError>;
fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), SupervisorError>;
fn zero(&mut self, ptr: u32, len: u32) -> Result<(), SupervisorError>;
fn read(&self, ptr: u32, len: u32) -> Result<Vec<u8>, SupervisorError> {
let mut buf = vec![0u8; len as usize];
self.read_into_buf(ptr, buf.as_mut_slice())?;
Ok(buf)
}
fn read_array<const N: usize>(&self, ptr: u32) -> Result<[u8; N], SupervisorError> {
let mut buf = [0u8; N];
self.read_into_buf(ptr, &mut buf)?;
Ok(buf)
}
fn read_u32(&self, ptr: u32) -> Result<u32, SupervisorError> {
let buf: [u8; 4] = self.read_array(ptr)?;
Ok(u32::from_le_bytes(buf))
}
fn read_u256(&self, ptr: u32) -> Result<U256, SupervisorError> {
let buf: [u8; 32] = self.read_array(ptr)?;
Ok(U256::from_little_endian(&buf))
}
fn read_h160(&self, ptr: u32) -> Result<H160, SupervisorError> {
let mut buf = H160::default();
self.read_into_buf(ptr, buf.as_bytes_mut())?;
Ok(buf)
}
fn read_h256(&self, ptr: u32) -> Result<H256, SupervisorError> {
let mut code_hash = H256::default();
self.read_into_buf(ptr, code_hash.as_bytes_mut())?;
Ok(code_hash)
}
}
pub trait PolkaVmInstance: Memory {
fn gas(&self) -> polkavm::Gas;
fn set_gas(&mut self, gas: polkavm::Gas);
fn read_input_regs(&self) -> (u64, u64, u64, u64, u64, u64);
fn write_output(&mut self, output: u64);
}
#[cfg(feature = "runtime-benchmarks")]
impl Memory for [u8] {
fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), SupervisorError> {
let ptr = ptr as usize;
let bound_checked = self
.get(ptr..ptr + buf.len())
.ok_or(SupervisorError::OutOfBounds)?;
buf.copy_from_slice(bound_checked);
Ok(())
}
fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), SupervisorError> {
let ptr = ptr as usize;
let bound_checked = self
.get_mut(ptr..ptr + buf.len())
.ok_or(SupervisorError::OutOfBounds)?;
bound_checked.copy_from_slice(buf);
Ok(())
}
fn zero(&mut self, ptr: u32, len: u32) -> Result<(), SupervisorError> {
<[u8] as Memory>::write(self, ptr, &vec![0; len as usize])
}
}
impl Memory for polkavm::RawInstance {
fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), SupervisorError> {
self.read_memory_into(ptr, buf)
.map(|_| ())
.map_err(|_| SupervisorError::OutOfBounds)
}
fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), SupervisorError> {
self.write_memory(ptr, buf)
.map_err(|_| SupervisorError::OutOfBounds)
}
fn zero(&mut self, ptr: u32, len: u32) -> Result<(), SupervisorError> {
self.zero_memory(ptr, len)
.map_err(|_| SupervisorError::OutOfBounds)
}
}
impl PolkaVmInstance for polkavm::RawInstance {
fn gas(&self) -> polkavm::Gas {
self.gas()
}
fn set_gas(&mut self, gas: polkavm::Gas) {
self.set_gas(gas)
}
fn read_input_regs(&self) -> (u64, u64, u64, u64, u64, u64) {
(
self.reg(polkavm::Reg::A0),
self.reg(polkavm::Reg::A1),
self.reg(polkavm::Reg::A2),
self.reg(polkavm::Reg::A3),
self.reg(polkavm::Reg::A4),
self.reg(polkavm::Reg::A5),
)
}
fn write_output(&mut self, output: u64) {
self.set_reg(polkavm::Reg::A0, output);
}
}
impl From<&ExecReturnValue> for ReturnErrorCode {
fn from(from: &ExecReturnValue) -> Self {
if from.flags.contains(ReturnFlags::REVERT) {
Self::CalleeReverted
} else {
Self::Success
}
}
}
#[derive(RuntimeDebug)]
pub struct ReturnData {
flags: u32,
data: Vec<u8>,
}
#[derive(RuntimeDebug)]
pub enum SupervisorError {
OutOfBounds,
ExecutionFailed,
ContractTrapped,
OutOfGas,
InvalidSyscall,
InvalidCallFlags,
StateChangeDenied,
InputForwarded,
NotPolkaVm,
CodeRejected,
}
#[derive(RuntimeDebug)]
pub enum TrapReason {
SupervisorError(SupervisorError),
Return(ReturnData),
}
impl From<SupervisorError> for TrapReason {
fn from(from: SupervisorError) -> Self {
TrapReason::SupervisorError(from)
}
}
impl fmt::Display for TrapReason {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
Ok(())
}
}
macro_rules! cost_args {
($name:ident, $( $arg: expr ),+) => {
(<T as Config>::WeightInfo::$name($( $arg ),+).saturating_sub(cost_args!(@call_zero $name, $( $arg ),+)))
};
(@call_zero $name:ident, $( $arg:expr ),*) => {
<T as Config>::WeightInfo::$name($( cost_args!(@replace_token $arg) ),*)
};
(@replace_token $_in:tt) => { 0 };
}
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[derive(Copy, Clone)]
pub enum RuntimeCosts {
HostFn,
CopyFromContract(u32),
CallDataLoad,
CallDataCopy(u32),
Caller,
CallDataSize,
Origin,
Address,
}
impl RuntimeCosts {
fn weight<T: Config>(&self) -> Weight {
use self::RuntimeCosts::*;
match *self {
HostFn => cost_args!(noop_host_fn, 1),
CopyFromContract(len) => <T as Config>::WeightInfo::seal_return(len),
CallDataSize => <T as Config>::WeightInfo::seal_call_data_size(),
CallDataLoad => <T as Config>::WeightInfo::seal_call_data_load(),
CallDataCopy(len) => <T as Config>::WeightInfo::seal_call_data_copy(len),
Caller => <T as Config>::WeightInfo::seal_caller(),
Origin => <T as Config>::WeightInfo::seal_origin(),
Address => <T as Config>::WeightInfo::seal_address(),
}
}
}
fn already_charged(_: u32) -> Option<RuntimeCosts> {
None
}
pub struct Runtime<'a, T, H, M: ?Sized> {
handle: &'a mut H,
input_data: Option<Vec<u8>>,
last_gas: polkavm::Gas,
_phantom_data: PhantomData<(T, M)>,
}
impl<'a, T: Config, H: PrecompileHandle, M: PolkaVmInstance> Runtime<'a, T, H, M> {
pub fn handle_interrupt(
&mut self,
interrupt: Result<polkavm::InterruptKind, polkavm::Error>,
module: &polkavm::Module,
instance: &mut M,
) -> Option<ExecResult> {
use polkavm::InterruptKind::*;
match interrupt {
Err(error) => {
log::error!(target: LOG_TARGET, "polkavm execution error: {error}");
Some(Err(SupervisorError::ExecutionFailed))
}
Ok(Finished) => Some(Ok(ExecReturnValue {
flags: ReturnFlags::empty(),
data: Vec::new(),
})),
Ok(Trap) => Some(Err(SupervisorError::ContractTrapped)),
Ok(Segfault(_)) => Some(Err(SupervisorError::ExecutionFailed)),
Ok(NotEnoughGas) => Some(Err(SupervisorError::OutOfGas)),
Ok(Step) => None,
Ok(Ecalli(idx)) => {
if cfg!(feature = "runtime-benchmarks") && idx == SENTINEL {
return Some(Ok(ExecReturnValue {
flags: ReturnFlags::empty(),
data: Vec::new(),
}));
}
let Some(syscall_symbol) = module.imports().get(idx) else {
return Some(Err(SupervisorError::InvalidSyscall));
};
match self.handle_ecall(instance, syscall_symbol.as_bytes()) {
Ok(None) => None,
Ok(Some(return_value)) => {
instance.write_output(return_value);
None
}
Err(TrapReason::Return(ReturnData { flags, data })) => {
match ReturnFlags::from_bits(flags) {
None => Some(Err(SupervisorError::InvalidCallFlags)),
Some(flags) => Some(Ok(ExecReturnValue { flags, data })),
}
}
Err(TrapReason::SupervisorError(error)) => Some(Err(error)),
}
}
}
}
}
impl<'a, T: Config, H: PrecompileHandle, M: PolkaVmInstance> Runtime<'a, T, H, M> {
pub fn new(handle: &'a mut H, input_data: Vec<u8>, gas_limit: polkavm::Gas) -> Self {
Self {
handle,
input_data: Some(input_data),
last_gas: gas_limit,
_phantom_data: Default::default(),
}
}
pub(crate) fn charge_gas(&mut self, costs: RuntimeCosts) -> Result<(), SupervisorError> {
let weight = costs.weight::<T>();
self.handle
.record_external_cost(Some(weight.ref_time()), Some(weight.proof_size()), None)
.map_err(|_| SupervisorError::OutOfGas)?;
Ok(())
}
pub(crate) fn charge_polkavm_gas(&mut self, memory: &mut M) -> Result<(), SupervisorError> {
let gas = self.last_gas - memory.gas();
if gas < 0 {
return Err(SupervisorError::OutOfGas);
}
self.handle
.record_cost(T::ConvertPolkaVmGas::polkavm_gas_to_evm_gas(gas))
.map_err(|_| SupervisorError::OutOfGas)?;
self.last_gas = memory.gas();
Ok(())
}
pub fn write_sandbox_output(
&mut self,
memory: &mut M,
out_ptr: u32,
out_len_ptr: u32,
buf: &[u8],
allow_skip: bool,
create_token: impl FnOnce(u32) -> Option<RuntimeCosts>,
) -> Result<(), SupervisorError> {
if allow_skip && out_ptr == SENTINEL {
return Ok(());
}
let len = memory.read_u32(out_len_ptr)?;
let buf_len = len.min(buf.len() as u32);
if let Some(costs) = create_token(buf_len) {
self.charge_gas(costs)?;
}
memory.write(out_ptr, &buf[..buf_len as usize])?;
memory.write(out_len_ptr, &buf_len.encode())
}
pub fn write_fixed_sandbox_output(
&mut self,
memory: &mut M,
out_ptr: u32,
buf: &[u8],
allow_skip: bool,
create_token: impl FnOnce(u32) -> Option<RuntimeCosts>,
) -> Result<(), SupervisorError> {
if buf.is_empty() || (allow_skip && out_ptr == SENTINEL) {
return Ok(());
}
let buf_len = buf.len() as u32;
if let Some(costs) = create_token(buf_len) {
self.charge_gas(costs)?;
}
memory.write(out_ptr, buf)
}
}
#[define_env]
pub mod env {
#[cfg(feature = "runtime-benchmarks")]
#[stable]
fn noop(&mut self, memory: &mut M) -> Result<(), TrapReason> {
Ok(())
}
#[stable]
fn call_data_size(&mut self, memory: &mut M) -> Result<u64, TrapReason> {
self.charge_gas(RuntimeCosts::CallDataSize)?;
Ok(self
.input_data
.as_ref()
.map(|input| input.len().try_into().expect("usize fits into u64; qed"))
.unwrap_or_default())
}
#[stable]
fn call_data_copy(
&mut self,
memory: &mut M,
out_ptr: u32,
out_len: u32,
offset: u32,
) -> Result<(), TrapReason> {
self.charge_gas(RuntimeCosts::CallDataCopy(out_len))?;
let Some(input) = self.input_data.as_ref() else {
return Err(SupervisorError::InputForwarded.into());
};
let start = offset as usize;
if start >= input.len() {
memory.zero(out_ptr, out_len)?;
return Ok(());
}
let end = start.saturating_add(out_len as usize).min(input.len());
memory.write(out_ptr, &input[start..end])?;
let bytes_written = (end - start) as u32;
memory.zero(
out_ptr.saturating_add(bytes_written),
out_len - bytes_written,
)?;
Ok(())
}
#[stable]
fn call_data_load(
&mut self,
memory: &mut M,
out_ptr: u32,
offset: u32,
) -> Result<(), TrapReason> {
self.charge_gas(RuntimeCosts::CallDataLoad)?;
let Some(input) = self.input_data.as_ref() else {
return Err(SupervisorError::InputForwarded.into());
};
let mut data = [0; 32];
let start = offset as usize;
let data = if start >= input.len() {
data } else {
let end = start.saturating_add(32).min(input.len());
data[..end - start].copy_from_slice(&input[start..end]);
data.reverse();
data };
self.write_fixed_sandbox_output(memory, out_ptr, &data, false, already_charged)?;
Ok(())
}
#[stable]
fn seal_return(
&mut self,
memory: &mut M,
flags: u32,
data_ptr: u32,
data_len: u32,
) -> Result<(), TrapReason> {
self.charge_gas(RuntimeCosts::CopyFromContract(data_len))?;
Err(TrapReason::Return(ReturnData {
flags,
data: memory.read(data_ptr, data_len)?,
}))
}
#[stable]
fn caller(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> {
self.charge_gas(RuntimeCosts::Caller)?;
let caller = self.handle.context().caller;
Ok(self.write_fixed_sandbox_output(
memory,
out_ptr,
caller.as_bytes(),
false,
already_charged,
)?)
}
#[stable]
fn origin(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> {
self.charge_gas(RuntimeCosts::Origin)?;
let origin = self.handle.origin();
Ok(self.write_fixed_sandbox_output(
memory,
out_ptr,
origin.as_bytes(),
false,
already_charged,
)?)
}
#[stable]
fn address(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> {
self.charge_gas(RuntimeCosts::Address)?;
let address = self.handle.context().address;
Ok(self.write_fixed_sandbox_output(
memory,
out_ptr,
address.as_bytes(),
false,
already_charged,
)?)
}
}