contracts: Allow runtime authors to define a chain extension (#7548)

* Make host functions return TrapReason

This avoids the need to manually store any trap reasons
to the `Runtime` from the host function. This adds the following
benefits:

* It properly composes with the upcoming chain extensions
* Missing to set a trap value is now a compile error

* Add chain extension

The chain extension is a way for the contract author to add new
host functions for contracts to call.

* Add tests for chain extensions

* Fix regression in set_rent.wat fixture

Not all offsets where properly updated when changing the fixtures
for the new salt on instantiate.

* Pre-charge a weight amount based off the specified length

* Improve fn write docs

* Renamed state to phantom

* Fix typo
This commit is contained in:
Alexander Theißen
2021-01-04 12:15:17 +01:00
committed by GitHub
parent e3e651f72c
commit 51c37ecc15
10 changed files with 768 additions and 37 deletions
+84 -19
View File
@@ -20,7 +20,7 @@
use crate::{
HostFnWeights, Schedule, Config, CodeHash, BalanceOf, Error,
exec::{Ext, StorageKey, TopicOf},
gas::{Gas, GasMeter, Token, GasMeterResult},
gas::{Gas, GasMeter, Token, GasMeterResult, ChargedAmount},
wasm::env_def::ConvertibleToWasm,
};
use sp_sandbox;
@@ -28,7 +28,7 @@ use parity_wasm::elements::ValueType;
use frame_system;
use frame_support::dispatch::DispatchError;
use sp_std::prelude::*;
use codec::{Decode, Encode};
use codec::{Decode, DecodeAll, Encode};
use sp_runtime::traits::SaturatedConversion;
use sp_core::crypto::UncheckedFrom;
use sp_io::hashing::{
@@ -126,7 +126,7 @@ impl<T: Into<DispatchError>> From<T> for TrapReason {
#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[derive(Copy, Clone)]
enum RuntimeToken {
pub enum RuntimeToken {
/// Charge the gas meter with the cost of a metering block. The charged costs are
/// the supplied cost of the block plus the overhead of the metering itself.
MeteringBlock(u32),
@@ -198,6 +198,10 @@ enum RuntimeToken {
HashBlake256(u32),
/// Weight of calling `seal_hash_blake2_128` for the given input size.
HashBlake128(u32),
/// Weight charged by a chain extension through `seal_call_chain_extension`.
ChainExtension(u64),
/// Weight charged for copying data from the sandbox.
CopyIn(u32),
}
impl<T: Config> Token<T> for RuntimeToken
@@ -256,6 +260,8 @@ where
.saturating_add(s.hash_blake2_256_per_byte.saturating_mul(len.into())),
HashBlake128(len) => s.hash_blake2_128
.saturating_add(s.hash_blake2_128_per_byte.saturating_mul(len.into())),
ChainExtension(amount) => amount,
CopyIn(len) => s.return_per_byte.saturating_mul(len.into()),
}
}
}
@@ -376,6 +382,14 @@ where
}
}
/// Get a mutable reference to the inner `Ext`.
///
/// This is mainly for the chain extension to have access to the environment the
/// contract is executing in.
pub fn ext(&mut self) -> &mut E {
self.ext
}
/// Store the reason for a host function triggered trap.
///
/// This is called by the `define_env` macro in order to store any error returned by
@@ -388,12 +402,12 @@ where
/// Charge the gas meter with the specified token.
///
/// Returns `Err(HostError)` if there is not enough gas.
fn charge_gas<Tok>(&mut self, token: Tok) -> Result<(), DispatchError>
pub fn charge_gas<Tok>(&mut self, token: Tok) -> Result<ChargedAmount, DispatchError>
where
Tok: Token<E::T, Metadata=HostFnWeights<E::T>>,
{
match self.gas_meter.charge(&self.schedule.host_fn_weights, token) {
GasMeterResult::Proceed => Ok(()),
GasMeterResult::Proceed(amount) => Ok(amount),
GasMeterResult::OutOfGas => Err(Error::<E::T>::OutOfGas.into())
}
}
@@ -403,7 +417,7 @@ where
/// Returns `Err` if one of the following conditions occurs:
///
/// - requested buffer is not within the bounds of the sandbox memory.
fn read_sandbox_memory(&self, ptr: u32, len: u32)
pub fn read_sandbox_memory(&self, ptr: u32, len: u32)
-> Result<Vec<u8>, DispatchError>
{
let mut buf = vec![0u8; len as usize];
@@ -417,7 +431,7 @@ where
/// Returns `Err` if one of the following conditions occurs:
///
/// - requested buffer is not within the bounds of the sandbox memory.
fn read_sandbox_memory_into_buf(&self, ptr: u32, buf: &mut [u8])
pub fn read_sandbox_memory_into_buf(&self, ptr: u32, buf: &mut [u8])
-> Result<(), DispatchError>
{
self.memory.get(ptr, buf).map_err(|_| Error::<E::T>::OutOfBounds.into())
@@ -429,20 +443,29 @@ where
///
/// - requested buffer is not within the bounds of the sandbox memory.
/// - the buffer contents cannot be decoded as the required type.
fn read_sandbox_memory_as<D: Decode>(&self, ptr: u32, len: u32)
///
/// # Note
///
/// It is safe to forgo benchmarking and charging weight relative to `len` for fixed
/// size types (basically everything not containing a heap collection):
/// Despite the fact that we are usually about to read the encoding of a fixed size
/// type, we cannot know the encoded size of that type. We therefore are required to
/// use the length provided by the contract. This length is untrusted and therefore
/// we charge weight relative to the provided size upfront that covers the copy costs.
/// On success this cost is refunded as the copying was already covered in the
/// overall cost of the host function. This is different from `read_sandbox_memory`
/// where the size is dynamic and the costs resulting from that dynamic size must
/// be charged relative to this dynamic size anyways (before reading) by constructing
/// the benchmark for that.
pub fn read_sandbox_memory_as<D: Decode>(&mut self, ptr: u32, len: u32)
-> Result<D, DispatchError>
{
let amount = self.charge_gas(RuntimeToken::CopyIn(len))?;
let buf = self.read_sandbox_memory(ptr, len)?;
D::decode(&mut &buf[..]).map_err(|_| Error::<E::T>::DecodingFailed.into())
}
/// Write the given buffer to 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 write_sandbox_memory(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> {
self.memory.set(ptr, buf).map_err(|_| Error::<E::T>::OutOfBounds.into())
let decoded = D::decode_all(&mut &buf[..])
.map_err(|_| DispatchError::from(Error::<E::T>::DecodingFailed))?;
self.gas_meter.refund(amount);
Ok(decoded)
}
/// Write the given buffer and its length to the designated locations in sandbox memory and
@@ -464,7 +487,7 @@ where
///
/// In addition to the error conditions of `write_sandbox_memory` this functions returns
/// `Err` if the size of the buffer located at `out_ptr` is too small to fit `buf`.
fn write_sandbox_output(
pub fn write_sandbox_output(
&mut self,
out_ptr: u32,
out_len_ptr: u32,
@@ -496,6 +519,15 @@ where
Ok(())
}
/// Write the given buffer to 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 write_sandbox_memory(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> {
self.memory.set(ptr, buf).map_err(|_| Error::<E::T>::OutOfBounds.into())
}
/// Computes the given hash function on the supplied input.
///
/// Reads from the sandboxed input buffer into an intermediate buffer.
@@ -1362,4 +1394,37 @@ define_env!(Env, <E: Ext>,
ctx.charge_gas(RuntimeToken::HashBlake128(input_len))?;
Ok(ctx.compute_hash_on_intermediate_buffer(blake2_128, input_ptr, input_len, output_ptr)?)
},
// Call into the chain extension provided by the chain if any.
//
// Handling of the input values is up to the specific chain extension and so is the
// return value. The extension can decide to use the inputs as primitive inputs or as
// in/out arguments by interpreting them as pointers. Any caller of this function
// must therefore coordinate with the chain that it targets.
//
// # Note
//
// If no chain extension exists the contract will trap with the `NoChainExtension`
// module error.
seal_call_chain_extension(
ctx,
func_id: u32,
input_ptr: u32,
input_len: u32,
output_ptr: u32,
output_len_ptr: u32
) -> u32 => {
use crate::chain_extension::{ChainExtension, Environment, RetVal};
if <E::T as Config>::ChainExtension::enabled() == false {
Err(Error::<E::T>::NoChainExtension)?;
}
let env = Environment::new(ctx, input_ptr, input_len, output_ptr, output_len_ptr);
match <E::T as Config>::ChainExtension::call(func_id, env)? {
RetVal::Converging(val) => Ok(val),
RetVal::Diverging{flags, data} => Err(TrapReason::Return(ReturnData {
flags: flags.bits(),
data,
})),
}
},
);