Sandboxing and the simplest smart-contract runtime (#140)

* Add primitives for sandboxing.

* Add sandbox module.

* Implement the runtime part of the sandbox.

* Rebuild binaries.

* Implement smart-contract execution.

* Add more documentation.
This commit is contained in:
Sergey Pepyakin
2018-05-01 21:32:01 +03:00
committed by Robert Habermeier
parent f116f67382
commit 5a56fbcea3
39 changed files with 2470 additions and 56 deletions
@@ -5,7 +5,7 @@ authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
hex-literal = "0.1.0"
integer-sqrt = "0.1.0"
integer-sqrt = { git = "https://github.com/paritytech/integer-sqrt-rs.git", branch = "master" }
serde = { version = "1.0", default_features = false }
safe-mix = { path = "../../../safe-mix", default_features = false}
substrate-keyring = { path = "../../keyring", optional = true }
@@ -5,7 +5,7 @@ authors = ["Parity Technologies <admin@parity.io>"]
[dependencies]
num-traits = { version = "0.2", default_features = false }
integer-sqrt = "0.1.0"
integer-sqrt = { git = "https://github.com/paritytech/integer-sqrt-rs.git", branch = "master" }
serde = { version = "1.0", optional = true }
serde_derive = { version = "1.0", optional = true }
substrate-codec = { path = "../../codec", default_features = false }
@@ -12,12 +12,16 @@ substrate-codec = { path = "../../codec", default_features = false }
substrate-primitives = { path = "../../primitives", default_features = false }
substrate-runtime-std = { path = "../../runtime-std", default_features = false }
substrate-runtime-io = { path = "../../runtime-io", default_features = false }
substrate-runtime-sandbox = { path = "../../runtime-sandbox", default_features = false }
substrate-runtime-support = { path = "../../runtime-support", default_features = false }
substrate-runtime-primitives = { path = "../primitives", default_features = false }
substrate-runtime-consensus = { path = "../consensus", default_features = false }
substrate-runtime-system = { path = "../system", default_features = false }
substrate-runtime-session = { path = "../session", default_features = false }
[dev-dependencies]
wabt = "0.1.7"
[features]
default = ["std"]
std = [
@@ -28,6 +32,7 @@ std = [
"substrate-primitives/std",
"substrate-runtime-std/std",
"substrate-runtime-io/std",
"substrate-runtime-sandbox/std",
"substrate-runtime-support/std",
"substrate-runtime-primitives/std",
"substrate-runtime-session/std",
@@ -0,0 +1,181 @@
// Copyright 2018 Parity Technologies (UK) Ltd.
// This file is part of Substrate.
// Substrate is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Substrate is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
//! Smart-contract execution module.
use codec::Slicable;
use primitives::traits::As;
use rstd::prelude::*;
use sandbox;
use {AccountDb, Module, OverlayAccountDb, Trait};
struct ExecutionExt<'a, 'b: 'a, T: Trait + 'b> {
account_db: &'a mut OverlayAccountDb<'b, T>,
account: T::AccountId,
memory: sandbox::Memory,
}
impl<'a, 'b: 'a, T: Trait> ExecutionExt<'a, 'b, T> {
fn account(&self) -> &T::AccountId {
&self.account
}
fn account_db(&self) -> &OverlayAccountDb<T> {
self.account_db
}
fn account_db_mut(&mut self) -> &mut OverlayAccountDb<'b, T> {
self.account_db
}
fn memory(&self) -> &sandbox::Memory {
&self.memory
}
}
pub(crate) fn execute<'a, 'b: 'a, T: Trait>(
code: &[u8],
account: &T::AccountId,
account_db: &'a mut OverlayAccountDb<'b, T>,
) -> bool {
// ext_put_storage(location_ptr: u32, value_non_null: u32, value_ptr: u32);
//
// Change the value at the given location in storage or remove it.
//
// - location_ptr: pointer into the linear
// memory where the location of the requested value is placed.
// - value_non_null: if set to 0, then the entry
// at the given location will be removed.
// - value_ptr: pointer into the linear memory
// where the value to set is placed. If `value_non_null` is set to 0, then this parameter is ignored.
fn ext_set_storage<T: Trait>(e: &mut ExecutionExt<T>, args: &[sandbox::TypedValue]) -> Result<sandbox::ReturnValue, sandbox::HostError> {
let location_ptr = args[0].as_i32().unwrap() as u32;
let value_non_null = args[1].as_i32().unwrap() as u32;
let value_ptr = args[2].as_i32().unwrap() as u32;
let mut location = [0; 32];
e.memory().get(location_ptr, &mut location)?;
let account = e.account().clone();
if value_non_null != 0 {
let mut value = [0; 32];
e.memory().get(value_ptr, &mut value)?;
e.account_db_mut()
.set_storage(&account, location.to_vec(), Some(value.to_vec()));
} else {
e.account_db_mut()
.set_storage(&account, location.to_vec(), None);
}
Ok(sandbox::ReturnValue::Unit)
}
// ext_get_storage(location_ptr: u32, dest_ptr: u32);
//
// Retrieve the value at the given location from the strorage.
// If there is no entry at the given location then all-zero-value
// will be returned.
//
// - location_ptr: pointer into the linear
// memory where the location of the requested value is placed.
// - dest_ptr: pointer where contents of the specified storage location
// should be placed.
fn ext_get_storage<T: Trait>(e: &mut ExecutionExt<T>, args: &[sandbox::TypedValue]) -> Result<sandbox::ReturnValue, sandbox::HostError> {
let location_ptr = args[0].as_i32().unwrap() as u32;
let dest_ptr = args[1].as_i32().unwrap() as u32;
let mut location = [0; 32];
e.memory().get(location_ptr, &mut location)?;
let account = e.account().clone();
if let Some(value) = e.account_db_mut().get_storage(&account, &location) {
e.memory().set(dest_ptr, &value)?;
} else {
e.memory().set(dest_ptr, &[0u8; 32])?;
}
Ok(sandbox::ReturnValue::Unit)
}
// ext_transfer(transfer_to: u32, transfer_to_len: u32, value: u32)
fn ext_transfer<T: Trait>(e: &mut ExecutionExt<T>, args: &[sandbox::TypedValue]) -> Result<sandbox::ReturnValue, sandbox::HostError> {
let transfer_to_ptr = args[0].as_i32().unwrap() as u32;
let transfer_to_len = args[1].as_i32().unwrap() as u32;
let value = args[2].as_i32().unwrap() as u64;
// TODO: slicable
let mut transfer_to = Vec::new();
transfer_to.resize(transfer_to_len as usize, 0);
e.memory().get(transfer_to_ptr, &mut transfer_to)?;
let value = T::Balance::sa(value as usize);
let transfer_to = T::AccountId::decode(&mut &transfer_to[..]).unwrap();
let account = e.account().clone();
if let Some(commit_state) =
Module::<T>::effect_transfer(&account, &transfer_to, value, e.account_db())
{
e.account_db_mut().merge(commit_state);
}
Ok(sandbox::ReturnValue::Unit)
}
// ext_create(code_ptr: u32, code_len: u32, value: u32)
fn ext_create<T: Trait>(e: &mut ExecutionExt<T>, args: &[sandbox::TypedValue]) -> Result<sandbox::ReturnValue, sandbox::HostError> {
let code_ptr = args[0].as_i32().unwrap() as u32;
let code_len = args[1].as_i32().unwrap() as u32;
let value = args[2].as_i32().unwrap() as u32;
// TODO: slicable
let value = T::Balance::sa(value as usize);
let mut code = Vec::new();
code.resize(code_len as usize, 0u8);
e.memory().get(code_ptr, &mut code)?;
let account = e.account().clone();
if let Some(commit_state) =
Module::<T>::effect_create(&account, &code, value, e.account_db())
{
e.account_db_mut().merge(commit_state);
}
Ok(sandbox::ReturnValue::Unit)
}
// TODO: Inspect the binary to extract the initial page count.
let memory = match sandbox::Memory::new(1, None) {
Ok(memory) => memory,
Err(_) => return false,
};
let mut imports = sandbox::EnvironmentDefinitionBuilder::new();
imports.add_host_func("env", "ext_set_storage", ext_set_storage::<T>);
imports.add_host_func("env", "ext_get_storage", ext_get_storage::<T>);
imports.add_host_func("env", "ext_transfer", ext_transfer::<T>);
imports.add_host_func("env", "ext_create", ext_create::<T>);
// TODO: ext_balance, ext_address, ext_callvalue, etc.
imports.add_memory("env", "memory", memory.clone());
let mut exec_ext = ExecutionExt {
account: account.clone(),
account_db,
memory,
};
let mut instance = match sandbox::Instance::new(code, &imports, &mut exec_ext) {
Ok(instance) => instance,
Err(_err) => return false,
};
instance.invoke(b"call", &[], &mut exec_ext).is_ok()
}
+223 -30
View File
@@ -21,6 +21,9 @@
#[cfg(feature = "std")]
extern crate serde;
#[cfg(test)]
extern crate wabt;
#[macro_use]
extern crate substrate_runtime_support as runtime_support;
@@ -32,6 +35,7 @@ extern crate substrate_primitives;
extern crate substrate_runtime_io as runtime_io;
extern crate substrate_runtime_primitives as primitives;
extern crate substrate_runtime_consensus as consensus;
extern crate substrate_runtime_sandbox as sandbox;
extern crate substrate_runtime_session as session;
extern crate substrate_runtime_system as system;
@@ -44,6 +48,8 @@ use codec::Slicable;
use runtime_support::{StorageValue, StorageMap, Parameter};
use primitives::traits::{Zero, One, Bounded, RefInto, SimpleArithmetic, Executable, MakePayment};
mod contract;
#[cfg(test)]
#[derive(Debug, PartialEq, Clone)]
pub enum LockStatus<BlockNumber: Debug + PartialEq + Clone> {
@@ -169,8 +175,8 @@ impl<T: Trait> Module<T> {
/// Create a smart-contract account.
pub fn create(aux: &T::PublicAux, code: &[u8], value: T::Balance) {
// commit anything that made it this far to storage
if let Some(commit) = Self::effect_create(aux.ref_into(), code, value, DirectAccountDb) {
<AccountDb<T>>::merge(&DirectAccountDb, commit);
if let Some(commit) = Self::effect_create(aux.ref_into(), code, value, &DirectAccountDb) {
<AccountDb<T>>::merge(&mut DirectAccountDb, commit);
}
}
@@ -180,8 +186,8 @@ impl<T: Trait> Module<T> {
/// TODO: probably want to state gas-limit and gas-price.
fn transfer(aux: &T::PublicAux, dest: T::AccountId, value: T::Balance) {
// commit anything that made it this far to storage
if let Some(commit) = Self::effect_transfer(aux.ref_into(), &dest, value, DirectAccountDb) {
<AccountDb<T>>::merge(&DirectAccountDb, commit);
if let Some(commit) = Self::effect_transfer(aux.ref_into(), &dest, value, &DirectAccountDb) {
<AccountDb<T>>::merge(&mut DirectAccountDb, commit);
}
}
@@ -382,11 +388,11 @@ trait AccountDb<T: Trait> {
fn get_code(&self, account: &T::AccountId) -> Vec<u8>;
fn get_balance(&self, account: &T::AccountId) -> T::Balance;
fn set_storage(&self, account: &T::AccountId, location: Vec<u8>, value: Option<Vec<u8>>);
fn set_code(&self, account: &T::AccountId, code: Vec<u8>);
fn set_balance(&self, account: &T::AccountId, balance: T::Balance);
fn set_storage(&mut self, account: &T::AccountId, location: Vec<u8>, value: Option<Vec<u8>>);
fn set_code(&mut self, account: &T::AccountId, code: Vec<u8>);
fn set_balance(&mut self, account: &T::AccountId, balance: T::Balance);
fn merge(&self, state: State<T>);
fn merge(&mut self, state: State<T>);
}
struct DirectAccountDb;
@@ -400,20 +406,20 @@ impl<T: Trait> AccountDb<T> for DirectAccountDb {
fn get_balance(&self, account: &T::AccountId) -> T::Balance {
<FreeBalance<T>>::get(account)
}
fn set_storage(&self, account: &T::AccountId, location: Vec<u8>, value: Option<Vec<u8>>) {
fn set_storage(&mut self, account: &T::AccountId, location: Vec<u8>, value: Option<Vec<u8>>) {
if let Some(value) = value {
<StorageOf<T>>::insert(&(account.clone(), location), &value);
} else {
<StorageOf<T>>::remove(&(account.clone(), location));
}
}
fn set_code(&self, account: &T::AccountId, code: Vec<u8>) {
fn set_code(&mut self, account: &T::AccountId, code: Vec<u8>) {
<CodeOf<T>>::insert(account, &code);
}
fn set_balance(&self, account: &T::AccountId, balance: T::Balance) {
fn set_balance(&mut self, account: &T::AccountId, balance: T::Balance) {
<FreeBalance<T>>::insert(account, balance);
}
fn merge(&self, s: State<T>) {
fn merge(&mut self, s: State<T>) {
for (address, changed) in s.into_iter() {
if let Some(balance) = changed.balance {
<FreeBalance<T>>::insert(&address, balance);
@@ -450,40 +456,50 @@ impl<'a, T: Trait> OverlayAccountDb<'a, T> {
}
impl<'a, T: Trait> AccountDb<T> for OverlayAccountDb<'a, T> {
fn get_storage(&self, account: &T::AccountId, location: &[u8]) -> Option<Vec<u8>> {
self.local.borrow().get(account)
self.local
.borrow()
.get(account)
.and_then(|a| a.storage.get(location))
.cloned()
.unwrap_or_else(|| self.underlying.get_storage(account, location))
}
fn get_code(&self, account: &T::AccountId) -> Vec<u8> {
self.local.borrow().get(account)
self.local
.borrow()
.get(account)
.and_then(|a| a.code.clone())
.unwrap_or_else(|| self.underlying.get_code(account))
}
fn get_balance(&self, account: &T::AccountId) -> T::Balance {
self.local.borrow().get(account)
self.local
.borrow()
.get(account)
.and_then(|a| a.balance)
.unwrap_or_else(|| self.underlying.get_balance(account))
}
fn set_storage(&self, account: &T::AccountId, location: Vec<u8>, value: Option<Vec<u8>>) {
self.local.borrow_mut()
fn set_storage(&mut self, account: &T::AccountId, location: Vec<u8>, value: Option<Vec<u8>>) {
self.local
.borrow_mut()
.entry(account.clone())
.or_insert(Default::default())
.storage.insert(location, value);
.storage
.insert(location, value);
}
fn set_code(&self, account: &T::AccountId, code: Vec<u8>) {
self.local.borrow_mut()
fn set_code(&mut self, account: &T::AccountId, code: Vec<u8>) {
self.local
.borrow_mut()
.entry(account.clone())
.or_insert(Default::default())
.code = Some(code);
}
fn set_balance(&self, account: &T::AccountId, balance: T::Balance) {
self.local.borrow_mut()
fn set_balance(&mut self, account: &T::AccountId, balance: T::Balance) {
self.local
.borrow_mut()
.entry(account.clone())
.or_insert(Default::default())
.balance = Some(balance);
}
fn merge(&self, s: State<T>) {
fn merge(&mut self, s: State<T>) {
let mut local = self.local.borrow_mut();
for (address, changed) in s.into_iter() {
@@ -511,7 +527,7 @@ impl<T: Trait> Module<T> {
transactor: &T::AccountId,
code: &[u8],
value: T::Balance,
account_db: DB
account_db: &DB,
) -> Option<State<T>> {
let from_balance = account_db.get_balance(transactor);
// TODO: a fee.
@@ -538,32 +554,34 @@ impl<T: Trait> Module<T> {
transactor: &T::AccountId,
dest: &T::AccountId,
value: T::Balance,
account_db: DB
account_db: &DB,
) -> Option<State<T>> {
let from_balance = account_db.get_balance(transactor);
assert!(from_balance >= value);
let to_balance = account_db.get_balance(dest);
assert!(<Bondage<T>>::get(transactor) <= <Bondage<T>>::get(dest));
assert!(to_balance + value > to_balance); // no overflow
assert!(to_balance + value > to_balance); // no overflow
// TODO: a fee, based upon gaslimit/gasprice.
// TODO: consider storing upper-bound for contract's gas limit in fixed-length runtime
// code in contract itself and use that.
// Our local overlay: Should be used for any transfers and creates that happen internally.
let overlay = OverlayAccountDb::new(&account_db);
let mut overlay = OverlayAccountDb::new(account_db);
if transactor != dest {
overlay.set_balance(transactor, from_balance - value);
overlay.set_balance(dest, to_balance + value);
}
let should_commit = {
let dest_code = overlay.get_code(dest);
let should_commit = if dest_code.is_empty() {
true
} else {
// TODO: logging (logs are just appended into a notable storage-based vector and cleared every
// block).
// TODO: if `overlay.get_code(dest)` isn't empty then execute code with `overlay`.
true
contract::execute(&dest_code, dest, &mut overlay)
};
if should_commit {
@@ -1025,4 +1043,179 @@ mod tests {
assert_eq!(Staking::free_balance(&2), 42);
});
}
#[test]
fn contract_transfer() {
let code_transfer = wabt::wat2wasm(
r#"
(module
;; ext_transfer(transfer_to: u32, transfer_to_len: u32, value: u32)
(import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32)))
(import "env" "memory" (memory 1))
(func (export "call")
(call $ext_transfer
(i32.const 4) ;; Pointer to "Transfer to" address.
(i32.const 8) ;; Length of "Transfer to" address.
(i32.const 6) ;; value to transfer
)
)
;; Destination AccountId to transfer the funds.
;; Represented by u64 (8 bytes long) in little endian.
(data (i32.const 4) "\02\00\00\00\00\00\00\00")
)
"#,
).unwrap();
with_externalities(&mut new_test_ext(1, 3, 1, false), || {
<FreeBalance<Test>>::insert(0, 111);
<FreeBalance<Test>>::insert(1, 0);
<FreeBalance<Test>>::insert(2, 30);
<CodeOf<Test>>::insert(1, code_transfer.to_vec());
Staking::transfer(&0, 1, 11);
assert_eq!(Staking::balance(&0), 100);
assert_eq!(Staking::balance(&1), 5);
assert_eq!(Staking::balance(&2), 36);
});
}
fn escaped_bytestring(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut result = String::new();
for b in bytes {
write!(result, "\\{:02x}", b).unwrap();
}
result
}
#[test]
fn contract_create() {
let code_transfer = wabt::wat2wasm(
r#"
(module
;; ext_transfer(transfer_to: u32, transfer_to_len: u32, value: u32)
(import "env" "ext_transfer" (func $ext_transfer (param i32 i32 i32)))
(import "env" "memory" (memory 1))
(func (export "call")
(call $ext_transfer
(i32.const 4) ;; Pointer to "Transfer to" address.
(i32.const 8) ;; Length of "Transfer to" address.
(i32.const 6) ;; value to transfer
)
)
;; Destination AccountId to transfer the funds.
;; Represented by u64 (8 bytes long) in little endian.
(data (i32.const 4) "\02\00\00\00\00\00\00\00")
)
"#,
).unwrap();
let code_create = wabt::wat2wasm(format!(
r#"
(module
;; ext_create(code_ptr: u32, code_len: u32, value: u32)
(import "env" "ext_create" (func $ext_create (param i32 i32 i32)))
(import "env" "memory" (memory 1))
(func (export "call")
(call $ext_create
(i32.const 4) ;; Pointer to `code`
(i32.const {code_length}) ;; Length of `code`
(i32.const 3) ;; Value to transfer
)
)
(data (i32.const 4) "{escaped_code_transfer}")
)
"#,
escaped_code_transfer = escaped_bytestring(&code_transfer),
code_length = code_transfer.len(),
)).unwrap();
with_externalities(&mut new_test_ext(1, 3, 1, false), || {
<FreeBalance<Test>>::insert(0, 111);
<FreeBalance<Test>>::insert(1, 0);
<CodeOf<Test>>::insert(1, code_create.to_vec());
// When invoked, the contract at address `1` must create a contract with 'transfer' code.
Staking::transfer(&0, 1, 11);
let derived_address =
<Test as Trait>::DetermineContractAddress::contract_address_for(&code_transfer, &1);
assert_eq!(Staking::balance(&0), 100);
assert_eq!(Staking::balance(&1), 8);
assert_eq!(Staking::balance(&derived_address), 3);
});
}
#[test]
fn contract_adder() {
let code_adder = wabt::wat2wasm(r#"
(module
;; ext_set_storage(location_ptr: i32, value_non_null: bool, value_ptr: i32)
(import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32)))
;; ext_get_storage(location_ptr: i32, value_ptr: i32)
(import "env" "ext_get_storage" (func $ext_get_storage (param i32 i32)))
(import "env" "memory" (memory 1))
(func (export "call")
(call $ext_get_storage
(i32.const 4) ;; Point to a location of the storage.
(i32.const 36) ;; The result will be written at this address.
)
(i32.store
(i32.const 36)
(i32.add
(i32.load
(i32.const 36)
)
(i32.const 1)
)
)
(call $ext_set_storage
(i32.const 4) ;; Pointer to a location of the storage.
(i32.const 1) ;; Value is not null.
(i32.const 36) ;; Pointer to a data we want to put in the storage.
)
)
;; Location of storage to put the data. 32 bytes.
(data (i32.const 4) "\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01")
)
"#).unwrap();
with_externalities(&mut new_test_ext(1, 3, 1, false), || {
<FreeBalance<Test>>::insert(0, 111);
<FreeBalance<Test>>::insert(1, 0);
<CodeOf<Test>>::insert(1, code_adder);
Staking::transfer(&0, 1, 1);
Staking::transfer(&0, 1, 1);
let storage_addr = [0x01u8; 32];
let value =
AccountDb::<Test>::get_storage(&DirectAccountDb, &1, &storage_addr).unwrap();
assert_eq!(
&value,
&[
2, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
]
);
});
}
}