Skip to content

Commit

Permalink
[pallet-revive] implement the call data load API (#6835)
Browse files Browse the repository at this point in the history
This PR implements the call data load API akin to [how it works on
ethereum](https://www.evm.codes/?fork=cancun#35).

There will also be a second PR to adjust the input function to resemble
the call data copy opcode on EVM.

---------

Signed-off-by: Cyrill Leutwiler <[email protected]>
Co-authored-by: command-bot <>
  • Loading branch information
xermicus authored Dec 12, 2024
1 parent 61518e0 commit 389e221
Show file tree
Hide file tree
Showing 8 changed files with 617 additions and 431 deletions.
12 changes: 12 additions & 0 deletions prdoc/pr_6835.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
title: '[pallet-revive] implement the call data load API'
doc:
- audience: Runtime Dev
description: |-
This PR implements the call data load API akin to [how it works on ethereum](https://www.evm.codes/?fork=cancun#35).
crates:
- name: pallet-revive-fixtures
bump: minor
- name: pallet-revive
bump: minor
- name: pallet-revive-uapi
bump: minor
44 changes: 44 additions & 0 deletions substrate/frame/revive/fixtures/contracts/call_data_load.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 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.

//! This uses the call data load API to first the first input byte.
//! This single input byte is used as the offset for a second call
//! to the call data load API.
//! The output of the second API call is returned.
#![no_std]
#![no_main]

extern crate common;
use uapi::{HostFn, HostFnImpl as api, ReturnFlags};

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn deploy() {}

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
let mut buf = [0; 32];
api::call_data_load(&mut buf, 0);

let offset = buf[31] as u32;
let mut buf = [0; 32];
api::call_data_load(&mut buf, offset);

api::return_value(ReturnFlags::empty(), &buf);
}
15 changes: 15 additions & 0 deletions substrate/frame/revive/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,21 @@ mod benchmarks {
assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().get_weight_price(weight));
}

#[benchmark(pov_mode = Measured)]
fn seal_call_data_load() {
let mut setup = CallSetup::<T>::default();
let (mut ext, _) = setup.ext();
let mut runtime = crate::wasm::Runtime::new(&mut ext, vec![42u8; 32]);
let mut memory = memory!(vec![0u8; 32],);
let result;
#[block]
{
result = runtime.bench_call_data_load(memory.as_mut_slice(), 0, 0);
}
assert_ok!(result);
assert_eq!(&memory[..], &vec![42u8; 32]);
}

#[benchmark(pov_mode = Measured)]
fn seal_input(n: Linear<0, { limits::code::BLOB_BYTES - 4 }>) {
let mut setup = CallSetup::<T>::default();
Expand Down
42 changes: 42 additions & 0 deletions substrate/frame/revive/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4407,6 +4407,48 @@ fn chain_id_works() {
});
}

#[test]
fn call_data_load_api_works() {
let (code, _) = compile_module("call_data_load").unwrap();

ExtBuilder::default().existential_deposit(100).build().execute_with(|| {
let _ = <Test as Config>::Currency::set_balance(&ALICE, 1_000_000);

// Create fixture: Constructor does nothing
let Contract { addr, .. } =
builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract();

// Call the contract: It reads a byte for the offset and then returns
// what call data load returned using this byte as the offset.
let input = (3u8, U256::max_value(), U256::max_value()).encode();
let received = builder::bare_call(addr).data(input).build().result.unwrap();
assert_eq!(received.flags, ReturnFlags::empty());
assert_eq!(U256::from_little_endian(&received.data), U256::max_value());

// Edge case
let input = (2u8, U256::from(255).to_big_endian()).encode();
let received = builder::bare_call(addr).data(input).build().result.unwrap();
assert_eq!(received.flags, ReturnFlags::empty());
assert_eq!(U256::from_little_endian(&received.data), U256::from(65280));

// Edge case
let received = builder::bare_call(addr).data(vec![1]).build().result.unwrap();
assert_eq!(received.flags, ReturnFlags::empty());
assert_eq!(U256::from_little_endian(&received.data), U256::zero());

// OOB case
let input = (42u8).encode();
let received = builder::bare_call(addr).data(input).build().result.unwrap();
assert_eq!(received.flags, ReturnFlags::empty());
assert_eq!(U256::from_little_endian(&received.data), U256::zero());

// No calldata should return the zero value
let received = builder::bare_call(addr).build().result.unwrap();
assert_eq!(received.flags, ReturnFlags::empty());
assert_eq!(U256::from_little_endian(&received.data), U256::zero());
});
}

#[test]
fn return_data_api_works() {
let (code_return_data_api, _) = compile_module("return_data_api").unwrap();
Expand Down
34 changes: 34 additions & 0 deletions substrate/frame/revive/src/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ pub enum RuntimeCosts {
CopyFromContract(u32),
/// Weight charged for copying data to the sandbox.
CopyToContract(u32),
/// Weight of calling `seal_call_data_load``.
CallDataLoad,
/// Weight of calling `seal_caller`.
Caller,
/// Weight of calling `seal_origin`.
Expand Down Expand Up @@ -429,6 +431,7 @@ impl<T: Config> Token<T> for RuntimeCosts {
HostFn => cost_args!(noop_host_fn, 1),
CopyToContract(len) => T::WeightInfo::seal_input(len),
CopyFromContract(len) => T::WeightInfo::seal_return(len),
CallDataLoad => T::WeightInfo::seal_call_data_load(),
Caller => T::WeightInfo::seal_caller(),
Origin => T::WeightInfo::seal_origin(),
IsContract => T::WeightInfo::seal_is_contract(),
Expand Down Expand Up @@ -1319,6 +1322,37 @@ pub mod env {
}
}

/// Stores the U256 value at given call input `offset` into the supplied buffer.
/// See [`pallet_revive_uapi::HostFn::call_data_load`].
#[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(Error::<E::T>::InputForwarded.into());
};

let mut data = [0; 32];
let start = offset as usize;
let data = if start >= input.len() {
data // Any index is valid to request; OOB offsets return zero.
} else {
let end = start.saturating_add(32).min(input.len());
data[..end - start].copy_from_slice(&input[start..end]);
data.reverse();
data // Solidity expects right-padded data
};

self.write_fixed_sandbox_output(memory, out_ptr, &data, false, already_charged)?;

Ok(())
}

/// Cease contract execution and save a data buffer as a result of the execution.
/// See [`pallet_revive_uapi::HostFn::return_value`].
#[stable]
Expand Down
Loading

0 comments on commit 389e221

Please sign in to comment.