Skip to content

Commit

Permalink
[pallet-revive] implement the call data copy API (#6880)
Browse files Browse the repository at this point in the history
This PR implements the call data copy API by adjusting the input method.

Closes #6770

---------

Signed-off-by: xermicus <[email protected]>
Co-authored-by: command-bot <>
  • Loading branch information
xermicus authored Dec 18, 2024
1 parent 08bfa86 commit 4a0e3f6
Show file tree
Hide file tree
Showing 15 changed files with 210 additions and 41 deletions.
14 changes: 14 additions & 0 deletions prdoc/pr_6880.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
title: '[pallet-revive] implement the call data copy API'
doc:
- audience: Runtime Dev
description: |-
This PR implements the call data copy API by adjusting the input method.

Closes #6770
crates:
- name: pallet-revive-fixtures
bump: major
- name: pallet-revive
bump: major
- name: pallet-revive-uapi
bump: major
53 changes: 53 additions & 0 deletions substrate/frame/revive/fixtures/contracts/call_data_copy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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.

//! Expects a call data of [0xFF; 32] and executes the test vectors from
//! [https://www.evm.codes/?fork=cancun#37] and some additional tests.
#![no_std]
#![no_main]

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

const TEST_DATA: [u8; 32] = [
255, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
];

#[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_copy(&mut &mut buf[..], 0);
assert_eq!(buf, [255; 32]);

api::call_data_copy(&mut &mut buf[..8], 31);
assert_eq!(buf, TEST_DATA);

api::call_data_copy(&mut &mut buf[..], 32);
assert_eq!(buf, [0; 32]);

let mut buf = [255; 32];
api::call_data_copy(&mut &mut buf[..], u32::MAX);
assert_eq!(buf, [0; 32]);
}
5 changes: 3 additions & 2 deletions substrate/frame/revive/fixtures/contracts/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,9 @@ macro_rules! input {
// e.g input!(buffer, 512, var1: u32, var2: [u8], );
($buffer:ident, $size:expr, $($rest:tt)*) => {
let mut $buffer = [0u8; $size];
let $buffer = &mut &mut $buffer[..];
$crate::api::input($buffer);
let input_size = $crate::u64_output!($crate::api::call_data_size,);
let $buffer = &mut &mut $buffer[..$size.min(input_size as usize)];
$crate::api::call_data_copy($buffer, 0);
input!(@inner $buffer, 0, $($rest)*);
};

Expand Down
Binary file modified substrate/frame/revive/rpc/examples/js/pvm/ErrorTester.polkavm
Binary file not shown.
Binary file modified substrate/frame/revive/rpc/examples/js/pvm/EventExample.polkavm
Binary file not shown.
Binary file modified substrate/frame/revive/rpc/examples/js/pvm/Flipper.polkavm
Binary file not shown.
Binary file not shown.
Binary file modified substrate/frame/revive/rpc/examples/js/pvm/PiggyBank.polkavm
Binary file not shown.
37 changes: 30 additions & 7 deletions substrate/frame/revive/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,10 @@ mod benchmarks {

// We just call a dummy contract to measure the overhead of the call extrinsic.
// The size of the data has no influence on the costs of this extrinsic as long as the contract
// won't call `seal_input` in its constructor to copy the data to contract memory.
// won't call `seal_call_data_copy` in its constructor to copy the data to contract memory.
// The dummy contract used here does not do this. The costs for the data copy is billed as
// part of `seal_input`. The costs for invoking a contract of a specific size are not part
// of this benchmark because we cannot know the size of the contract when issuing a call
// part of `seal_call_data_copy`. The costs for invoking a contract of a specific size are not
// part of this benchmark because we cannot know the size of the contract when issuing a call
// transaction. See `call_with_code_per_byte` for this.
#[benchmark(pov_mode = Measured)]
fn call() -> Result<(), BenchmarkError> {
Expand Down Expand Up @@ -853,6 +853,29 @@ mod benchmarks {
assert_eq!(U256::from_little_endian(&memory[..]), runtime.ext().get_weight_price(weight));
}

#[benchmark(pov_mode = Measured)]
fn seal_copy_to_contract(n: Linear<0, { limits::code::BLOB_BYTES - 4 }>) {
let mut setup = CallSetup::<T>::default();
let (mut ext, _) = setup.ext();
let mut runtime = crate::wasm::Runtime::new(&mut ext, vec![]);
let mut memory = memory!(n.encode(), vec![0u8; n as usize],);
let result;
#[block]
{
result = runtime.write_sandbox_output(
memory.as_mut_slice(),
4,
0,
&vec![42u8; n as usize],
false,
|_| None,
);
}
assert_ok!(result);
assert_eq!(&memory[..4], &n.encode());
assert_eq!(&memory[4..], &vec![42u8; n as usize]);
}

#[benchmark(pov_mode = Measured)]
fn seal_call_data_load() {
let mut setup = CallSetup::<T>::default();
Expand All @@ -869,18 +892,18 @@ mod benchmarks {
}

#[benchmark(pov_mode = Measured)]
fn seal_input(n: Linear<0, { limits::code::BLOB_BYTES - 4 }>) {
fn seal_call_data_copy(n: Linear<0, { limits::code::BLOB_BYTES }>) {
let mut setup = CallSetup::<T>::default();
let (mut ext, _) = setup.ext();
let mut runtime = crate::wasm::Runtime::new(&mut ext, vec![42u8; n as usize]);
let mut memory = memory!(n.to_le_bytes(), vec![0u8; n as usize],);
let mut memory = memory!(vec![0u8; n as usize],);
let result;
#[block]
{
result = runtime.bench_input(memory.as_mut_slice(), 4, 0);
result = runtime.bench_call_data_copy(memory.as_mut_slice(), 0, n, 0);
}
assert_ok!(result);
assert_eq!(&memory[4..], &vec![42u8; n as usize]);
assert_eq!(&memory[..], &vec![42u8; n as usize]);
}

#[benchmark(pov_mode = Measured)]
Expand Down
16 changes: 16 additions & 0 deletions substrate/frame/revive/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4364,6 +4364,22 @@ fn call_data_size_api_works() {
});
}

#[test]
fn call_data_copy_api_works() {
let (code, _) = compile_module("call_data_copy").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 fixture: Expects an input of [255; 32] and executes tests.
assert_ok!(builder::call(addr).data(vec![255; 32]).build());
});
}

#[test]
fn static_data_limit_is_enforced() {
let (oom_rw_trailing, _) = compile_module("oom_rw_trailing").unwrap();
Expand Down
56 changes: 45 additions & 11 deletions substrate/frame/revive/src/wasm/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ pub trait Memory<T: Config> {
/// - designated area is not within the bounds of the sandbox memory.
fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError>;

/// Zero the designated location in the sandbox memory.
///
/// Returns `Err` if one of the following conditions occurs:
///
/// - designated area is not within the bounds of the sandbox memory.
fn zero(&mut self, ptr: u32, len: u32) -> Result<(), DispatchError>;

/// Read designated chunk from the sandbox memory.
///
/// Returns `Err` if one of the following conditions occurs:
Expand Down Expand Up @@ -162,6 +169,10 @@ impl<T: Config> Memory<T> for [u8] {
bound_checked.copy_from_slice(buf);
Ok(())
}

fn zero(&mut self, ptr: u32, len: u32) -> Result<(), DispatchError> {
<[u8] as Memory<T>>::write(self, ptr, &vec![0; len as usize])
}
}

impl<T: Config> Memory<T> for polkavm::RawInstance {
Expand All @@ -174,6 +185,10 @@ impl<T: Config> Memory<T> for polkavm::RawInstance {
fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> {
self.write_memory(ptr, buf).map_err(|_| Error::<T>::OutOfBounds.into())
}

fn zero(&mut self, ptr: u32, len: u32) -> Result<(), DispatchError> {
self.zero_memory(ptr, len).map_err(|_| Error::<T>::OutOfBounds.into())
}
}

impl<T: Config> PolkaVmInstance<T> for polkavm::RawInstance {
Expand Down Expand Up @@ -269,6 +284,8 @@ pub enum RuntimeCosts {
CopyToContract(u32),
/// Weight of calling `seal_call_data_load``.
CallDataLoad,
/// Weight of calling `seal_call_data_copy`.
CallDataCopy(u32),
/// Weight of calling `seal_caller`.
Caller,
/// Weight of calling `seal_call_data_size`.
Expand Down Expand Up @@ -431,10 +448,11 @@ impl<T: Config> Token<T> for RuntimeCosts {
use self::RuntimeCosts::*;
match *self {
HostFn => cost_args!(noop_host_fn, 1),
CopyToContract(len) => T::WeightInfo::seal_input(len),
CopyToContract(len) => T::WeightInfo::seal_copy_to_contract(len),
CopyFromContract(len) => T::WeightInfo::seal_return(len),
CallDataSize => T::WeightInfo::seal_call_data_size(),
CallDataLoad => T::WeightInfo::seal_call_data_load(),
CallDataCopy(len) => T::WeightInfo::seal_call_data_copy(len),
Caller => T::WeightInfo::seal_caller(),
Origin => T::WeightInfo::seal_origin(),
IsContract => T::WeightInfo::seal_is_contract(),
Expand Down Expand Up @@ -1276,18 +1294,34 @@ pub mod env {
}

/// Stores the input passed by the caller into the supplied buffer.
/// See [`pallet_revive_uapi::HostFn::input`].
/// See [`pallet_revive_uapi::HostFn::call_data_copy`].
#[stable]
fn input(&mut self, memory: &mut M, out_ptr: u32, out_len_ptr: u32) -> Result<(), TrapReason> {
if let Some(input) = self.input_data.take() {
self.write_sandbox_output(memory, out_ptr, out_len_ptr, &input, false, |len| {
Some(RuntimeCosts::CopyToContract(len))
})?;
self.input_data = Some(input);
Ok(())
} else {
Err(Error::<E::T>::InputForwarded.into())
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(Error::<E::T>::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(())
}

/// Stores the U256 value at given call input `offset` into the supplied buffer.
Expand Down
35 changes: 28 additions & 7 deletions substrate/frame/revive/src/weights.rs

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

2 changes: 1 addition & 1 deletion substrate/frame/revive/uapi/src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ bitflags! {
///
/// A forwarding call will consume the current contracts input. Any attempt to
/// access the input after this call returns will lead to [`Error::InputForwarded`].
/// It does not matter if this is due to calling `seal_input` or trying another
/// It does not matter if this is due to calling `call_data_copy` or trying another
/// forwarding call. Consider using [`Self::CLONE_INPUT`] in order to preserve
/// the input.
const FORWARD_INPUT = 0b0000_0001;
Expand Down
18 changes: 14 additions & 4 deletions substrate/frame/revive/uapi/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,18 +245,28 @@ pub trait HostFn: private::Sealed {

hash_fn!(keccak_256, 32);

/// Stores the input passed by the caller into the supplied buffer.
/// Stores the input data passed by the caller into the supplied `output` buffer,
/// starting from the given input data `offset`.
///
/// The `output` buffer is guaranteed to always be fully populated:
/// - If the call data (starting from the given `offset`) is larger than the `output` buffer,
/// only what fits into the `output` buffer is written.
/// - If the `output` buffer size exceeds the call data size (starting from `offset`), remaining
/// bytes in the `output` buffer are zeroed out.
/// - If the provided call data `offset` is out-of-bounds, the whole `output` buffer is zeroed
/// out.
///
/// # Note
///
/// This function traps if:
/// - the input is larger than the available space.
/// - the input was previously forwarded by a [`call()`][`Self::call()`].
/// - the `output` buffer is located in an PolkaVM invalid memory range.
///
/// # Parameters
///
/// - `output`: A reference to the output data buffer to write the input data.
fn input(output: &mut &mut [u8]);
/// - `output`: A reference to the output data buffer to write the call data.
/// - `offset`: The offset index into the call data from where to start copying.
fn call_data_copy(output: &mut [u8], offset: u32);

/// Stores the U256 value at given `offset` from the input passed by the caller
/// into the supplied buffer.
Expand Down
Loading

0 comments on commit 4a0e3f6

Please sign in to comment.