Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pallet-revive] last call return data API #5779

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
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
166 changes: 166 additions & 0 deletions substrate/frame/revive/fixtures/contracts/return_data_api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// 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 tests that the `return_data_size` and `return_data_copy` APIs work.
//!
//! It does so by calling and instantiating the "return_with_data" fixture,
//! which always echoes back the input[4..] regardless of the call outcome.
//!
//! We also check that the saved return data is properly reset after a trap
//! and unaffected by plain transfers.

#![no_std]
#![no_main]

use common::{input, u256_bytes};
use uapi::{HostFn, HostFnImpl as api};

const INPUT_BUF_SIZE: usize = 128;
static INPUT_DATA: [u8; INPUT_BUF_SIZE] = [0xFF; INPUT_BUF_SIZE];
/// The "return_with_data" fixture echoes back 4 bytes less than the input
const OUTPUT_BUF_SIZE: usize = INPUT_BUF_SIZE - 4;
static OUTPUT_DATA: [u8; OUTPUT_BUF_SIZE] = [0xEE; OUTPUT_BUF_SIZE];

fn assert_return_data_after_call(input: &[u8]) {
assert_return_data_size_of(OUTPUT_BUF_SIZE as u64);
assert_plain_transfer_does_not_reset(OUTPUT_BUF_SIZE as u64);
assert_return_data_copy(&input[4..]);
reset_return_data();
}

/// Assert that what we get from [api::return_data_copy] matches `whole_return_data`,
/// either fully or partially with an offset and limited size.
fn assert_return_data_copy(whole_return_data: &[u8]) {
// The full return data should match
let mut buf = OUTPUT_DATA;
let mut full = &mut buf[..whole_return_data.len()];
api::return_data_copy(&mut full, 0);
assert_eq!(whole_return_data, full);

// Partial return data should match
let mut buf = OUTPUT_DATA;
let offset = 5; // we just pick some offset
let size = 32; // we just pick some size
let mut partial = &mut buf[offset..offset + size];
api::return_data_copy(&mut partial, offset as u32);
assert_eq!(*partial, whole_return_data[offset..offset + size]);
}

/// This function panics in a recursive contract call context.
fn recursion_guard() -> [u8; 20] {
let mut caller_address = [0u8; 20];
api::caller(&mut caller_address);

let mut own_address = [0u8; 20];
api::address(&mut own_address);

assert_ne!(caller_address, own_address);

own_address
}

/// Call ourselves recursively, which panics the callee and thus resets the return data.
fn reset_return_data() {
api::call(
uapi::CallFlags::ALLOW_REENTRY,
&recursion_guard(),
0u64,
0u64,
None,
&[0u8; 32],
&[0u8; 32],
None,
)
.unwrap_err();
assert_return_data_size_of(0);
}

/// Assert [api::return_data_size] to match the `expected` value.
fn assert_return_data_size_of(expected: u64) {
let mut return_data_size = [0xff; 32];
api::return_data_size(&mut return_data_size);
assert_eq!(return_data_size, u256_bytes(expected));
}

/// Assert [api::return_data_size] to match the `expected` value after a plain transfer
/// (plain transfers don't issue a call and so should not reset the return data)
fn assert_plain_transfer_does_not_reset(expected: u64) {
api::transfer(&[0; 20], &u256_bytes(128)).unwrap();
assert_return_data_size_of(expected);
}

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

#[no_mangle]
#[polkavm_derive::polkavm_export]
pub extern "C" fn call() {
input!(code_hash: &[u8; 32],);

recursion_guard();

// we didn't do anything yet; return data size should be 0
assert_return_data_size_of(0);

let mut address_buf = [0; 20];
let construct_input = |exit_flag| {
let mut input = INPUT_DATA;
input[0] = exit_flag;
input[9] = 7;
input[17 / 2] = 127;
input[89 / 2] = 127;
input
};
let mut instantiate = |exit_flag| {
api::instantiate(
code_hash,
0u64,
0u64,
None,
&[0; 32],
&construct_input(exit_flag),
Some(&mut address_buf),
None,
None,
)
};
let call = |exit_flag, address_buf| {
api::call(
uapi::CallFlags::empty(),
address_buf,
0u64,
0u64,
None,
&[0; 32],
&construct_input(exit_flag),
None,
)
};

instantiate(0).unwrap();
assert_return_data_after_call(&construct_input(0)[..]);

instantiate(1).unwrap_err();
assert_return_data_after_call(&construct_input(1)[..]);

call(0, &address_buf).unwrap();
assert_return_data_after_call(&construct_input(0)[..]);

call(1, &address_buf).unwrap_err();
assert_return_data_after_call(&construct_input(1)[..]);
}
29 changes: 29 additions & 0 deletions substrate/frame/revive/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,12 @@ pub trait Ext: sealing::Sealed {

/// Check if running in read-only context.
fn is_read_only(&self) -> bool;

/// Returns an immutable reference to saved output of the last executed call frame.
fn last_frame_output(&self) -> &[u8];

/// Returns a mutable reference to saved output of the last executed call frame.
fn last_frame_output_mut(&mut self) -> &mut Vec<u8>;
}

/// Describes the different functions that can be exported by an [`Executable`].
Expand Down Expand Up @@ -547,6 +553,8 @@ struct Frame<T: Config> {
read_only: bool,
/// The caller of the currently executing frame which was spawned by `delegate_call`.
delegate_caller: Option<Origin<T>>,
/// The output of the last call frame
last_frame_output: Vec<u8>,
}

/// Used in a delegate call frame arguments in order to override the executable and caller.
Expand Down Expand Up @@ -911,6 +919,7 @@ where
nested_storage: storage_meter.nested(deposit_limit),
allows_reentry: true,
read_only,
last_frame_output: Vec::new(),
};

Ok(Some((frame, executable)))
Expand Down Expand Up @@ -971,6 +980,9 @@ where
let delegated_code_hash =
if frame.delegate_caller.is_some() { Some(*executable.code_hash()) } else { None };

if let Some(previous) = self.caller_output_mut() {
*previous = Vec::new();
}
self.transient_storage.start_transaction();

let do_transaction = || {
Expand Down Expand Up @@ -1265,6 +1277,15 @@ where
fn account_balance(&self, who: &T::AccountId) -> U256 {
T::Currency::reducible_balance(who, Preservation::Preserve, Fortitude::Polite).into()
}

/// Returns a mutable reference to saved output of the caller frame.
fn caller_output_mut(&mut self) -> Option<&mut Vec<u8>> {
match self.frames.len() {
0 => None,
1 => Some(&mut self.first_frame.last_frame_output),
_ => self.frames.get_mut(self.frames.len() - 2).map(|frame| &mut frame.last_frame_output),
}
}
}

impl<'a, T, E> Ext for Stack<'a, T, E>
Expand Down Expand Up @@ -1690,6 +1711,14 @@ where
fn is_read_only(&self) -> bool {
self.top_frame().read_only
}

fn last_frame_output(&self) -> &[u8] {
&self.top_frame().last_frame_output
}

fn last_frame_output_mut(&mut self) -> &mut Vec<u8> {
&mut self.top_frame_mut().last_frame_output
}
}

mod sealing {
Expand Down
28 changes: 28 additions & 0 deletions substrate/frame/revive/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4310,4 +4310,32 @@ mod run_tests {
assert_ok!(builder::call(addr).build());
});
}

#[test]
fn return_data_api_works() {
let (code_return_data_api, _) = compile_module("return_data_api").unwrap();
let (code_return_with_data, hash_return_with_data) = compile_module("return_with_data").unwrap();

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

// Upload the io echoing fixture for later use
assert_ok!(Contracts::upload_code(
RuntimeOrigin::signed(ALICE),
code_return_with_data,
deposit_limit::<Test>(),
));

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

// Call the contract: It will issue calls and deploys, asserting on
assert_ok!(builder::call(addr)
.value(10 * 1024)
.data(hash_return_with_data.encode())
.build());
});
}
}
Loading
Loading