Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading