diff --git a/core/state-machine/src/testing.rs b/core/state-machine/src/testing.rs index 3adf277bf35d6..f8f81cfda1397 100644 --- a/core/state-machine/src/testing.rs +++ b/core/state-machine/src/testing.rs @@ -159,7 +159,7 @@ impl Externalities for TestExternalities where H::Out: Ord + He } fn child_storage_root(&mut self, _storage_key: ChildStorageKey) -> Vec { - unimplemented!() + vec![] } fn storage_changes_root(&mut self, parent: H::Out, parent_num: u64) -> Option { diff --git a/node/cli/src/chain_spec.rs b/node/cli/src/chain_spec.rs index 0c017d2fac979..f376db13c467b 100644 --- a/node/cli/src/chain_spec.rs +++ b/node/cli/src/chain_spec.rs @@ -152,6 +152,12 @@ fn staging_testnet_config_genesis() -> GenesisConfig { burn: Permill::from_percent(50), }), contract: Some(ContractConfig { + signed_claim_handicap: 2, + rent_byte_price: 4, + rent_deposit_offset: 1000, + storage_size_offset: 8, + surcharge_reward: 150, + tombstone_deposit: 16, transaction_base_fee: 1 * CENTS, transaction_byte_fee: 10 * MILLICENTS, transfer_fee: 1 * CENTS, @@ -239,6 +245,12 @@ pub fn testnet_genesis( const ENDOWMENT: u128 = 1 << 20; let mut contract_config = ContractConfig { + signed_claim_handicap: 2, + rent_byte_price: 4, + rent_deposit_offset: 1000, + storage_size_offset: 8, + surcharge_reward: 150, + tombstone_deposit: 16, transaction_base_fee: 1, transaction_byte_fee: 0, transfer_fee: 0, diff --git a/node/executor/src/lib.rs b/node/executor/src/lib.rs index d1a8e0cda3583..9fe26123ff43d 100644 --- a/node/executor/src/lib.rs +++ b/node/executor/src/lib.rs @@ -745,7 +745,13 @@ mod tests { runtime_io::with_externalities(&mut t, || { // Verify that the contract constructor worked well and code of TRANSFER contract is actually deployed. - assert_eq!(&contract::CodeHashOf::::get(addr).unwrap(), &transfer_ch); + assert_eq!( + &contract::ContractInfoOf::::get(addr) + .and_then(|c| c.get_alive()) + .unwrap() + .code_hash, + &transfer_ch + ); }); } diff --git a/node/runtime/src/lib.rs b/node/runtime/src/lib.rs index 39a6ba13866dd..3ec297b17b7c8 100644 --- a/node/runtime/src/lib.rs +++ b/node/runtime/src/lib.rs @@ -59,7 +59,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: create_runtime_str!("substrate-node"), authoring_version: 10, spec_version: 67, - impl_version: 67, + impl_version: 68, apis: RUNTIME_API_VERSIONS, }; diff --git a/srml/contract/COMPLEXITY.md b/srml/contract/COMPLEXITY.md index 9b304055f766c..3cd7fee448204 100644 --- a/srml/contract/COMPLEXITY.md +++ b/srml/contract/COMPLEXITY.md @@ -85,7 +85,7 @@ execution contexts operate on the AccountDb. All changes are flushed into underl Today `AccountDb` is implemented as a cascade of overlays with the direct storage at the bottom. Each overlay is represented by a `Map`. On a commit from an overlay to an overlay, maps are merged. On commit from an overlay to the bottommost `AccountDb` all changes are flushed to the storage. On revert, the overlay is just discarded. -## get_storage, get_code, get_balance +## get_storage, get_code_hash, get_rent_allowance, get_balance, contract_exists These functions check the local cache for a requested value and, if it is there, the value is returned. Otherwise, these functions will ask an underlying `AccountDb` for the value. This means that the number of lookups is proportional to the depth of the overlay cascade. If the value can't be found before reaching the bottommost `AccountDb`, then a DB read will be performed (in case `get_balance` the function `free_balance` will be invoked). @@ -95,7 +95,7 @@ These functions return an owned value as its result, so memory usage depends on **complexity**: The memory complexity is proportional to the size of the value. The computational complexity is proportional to the depth of the overlay cascade and the size of the value; the cost is dominated by the DB read though. -## set_storage, set_code, set_balance +## set_storage, set_balance, set_rent_allowance These functions only modify the local `Map`. @@ -105,6 +105,12 @@ While these functions only modify the local `Map`, if changes made by them are c **complexity**: Each lookup has a logarithmical computing time to the number of already inserted entries. No additional memory is required. +## create_contract + +Calls `contract_exists` and if it doesn't exist, do not modify the local `Map` similarly to `set_rent_allowance`. + +**complexity**: The computational complexity is proportional to the depth of the overlay cascade and the size of the value; the cost is dominated by the DB read though. No additional memory is required. + ## commit In this function, all cached values will be inserted into the underlying `AccountDb` or into the storage. @@ -327,3 +333,25 @@ This function copies slice of data from the scratch buffer to the sandbox memory 1. Storing a specified slice of the scratch buffer into the sandbox memory (see sandboxing memory set) **complexity**: The computing complexity of this function is proportional to the length of the slice. No additional memory is required. + +## ext_set_rent_allowance + +This function receives the following argument: + +- `value` buffer of a marshaled `Balance`, + +It consists of the following steps: + +1. Loading `value` buffer from the sandbox memory and then decoding it. +2. Invoking `set_rent_allowance` AccountDB function. + +**complexity**: Complexity is proportional to the size of the `value`. This function induces a DB write of size proportional to the `value` size (if flushed to the storage), so should be priced accordingly. + +## ext_rent_allowance + +It consists of the following steps: + +1. Invoking `get_rent_allowance` AccountDB function. +2. Serializing the rent allowance of the current contract into the scratch buffer. + +**complexity**: Assuming that the rent allowance is of constant size, this function has constant complexity. diff --git a/srml/contract/src/account_db.rs b/srml/contract/src/account_db.rs index 4f034a6f05a87..b94125d6dc40b 100644 --- a/srml/contract/src/account_db.rs +++ b/srml/contract/src/account_db.rs @@ -16,21 +16,28 @@ //! Auxilliaries to help with managing partial changes to accounts state. -use super::{CodeHash, CodeHashOf, Trait, TrieId, AccountInfoOf, BalanceOf, AccountInfo, TrieIdGenerator}; +use super::{ + AliveContractInfo, BalanceOf, CodeHash, ContractInfo, ContractInfoOf, Module, Trait, TrieId, + TrieIdGenerator, +}; use crate::exec::StorageKey; -use system; use rstd::cell::RefCell; use rstd::collections::btree_map::{BTreeMap, Entry}; use rstd::prelude::*; use runtime_io::blake2_256; use runtime_primitives::traits::Zero; -use srml_support::{StorageMap, traits::{UpdateBalanceOutcome, - SignedImbalance, Currency, Imbalance}, storage::child}; +use srml_support::traits::{Currency, Imbalance, SignedImbalance, UpdateBalanceOutcome}; +use srml_support::{storage::child, StorageMap}; +use system; +// Note: we don't provide Option because we can't create +// the trie_id in the overlay, thus we provide an overlay on the fields +// specifically. pub struct ChangeEntry { balance: Option>, - /// In the case the outer option is None, the code_hash remains untouched, while providing `Some(None)` signifies a removing of the code in question - code: Option>>, + /// If None, the code_hash remains untouched. + code_hash: Option>, + rent_allowance: Option>, storage: BTreeMap>>, } @@ -38,8 +45,9 @@ pub struct ChangeEntry { impl Default for ChangeEntry { fn default() -> Self { ChangeEntry { + rent_allowance: Default::default(), balance: Default::default(), - code: Default::default(), + code_hash: Default::default(), storage: Default::default(), } } @@ -51,10 +59,15 @@ pub trait AccountDb { /// Account is used when overlayed otherwise trie_id must be provided. /// This is for performance reason. /// - /// Trie id can be None iff account doesn't have an associated trie id in >. + /// Trie id is None iff account doesn't have an associated trie id in >. /// Because DirectAccountDb bypass the lookup for this association. fn get_storage(&self, account: &T::AccountId, trie_id: Option<&TrieId>, location: &StorageKey) -> Option>; - fn get_code(&self, account: &T::AccountId) -> Option>; + /// If account has an alive contract then return the code hash associated. + fn get_code_hash(&self, account: &T::AccountId) -> Option>; + /// If account has an alive contract then return the rent allowance associated. + fn get_rent_allowance(&self, account: &T::AccountId) -> Option>; + /// Returns false iff account has no alive contract nor tombstone. + fn contract_exists(&self, account: &T::AccountId) -> bool; fn get_balance(&self, account: &T::AccountId) -> BalanceOf; fn commit(&mut self, change_set: ChangeSet); @@ -65,8 +78,14 @@ impl AccountDb for DirectAccountDb { fn get_storage(&self, _account: &T::AccountId, trie_id: Option<&TrieId>, location: &StorageKey) -> Option> { trie_id.and_then(|id| child::get_raw(id, &blake2_256(location))) } - fn get_code(&self, account: &T::AccountId) -> Option> { - >::get(account) + fn get_code_hash(&self, account: &T::AccountId) -> Option> { + >::get(account).and_then(|i| i.as_alive().map(|i| i.code_hash)) + } + fn get_rent_allowance(&self, account: &T::AccountId) -> Option> { + >::get(account).and_then(|i| i.as_alive().map(|i| i.rent_allowance)) + } + fn contract_exists(&self, account: &T::AccountId) -> bool { + >::exists(account) } fn get_balance(&self, account: &T::AccountId) -> BalanceOf { T::Currency::free_balance(account) @@ -84,42 +103,58 @@ impl AccountDb for DirectAccountDb { continue; } } - if changed.code.is_some() || !changed.storage.is_empty() { - let mut info = if !>::exists(&address) { - let info = AccountInfo { - trie_id: ::TrieIdGenerator::trie_id(&address), - storage_size: 0, - }; - >::insert(&address, &info); + + if changed.code_hash.is_some() + || changed.rent_allowance.is_some() + || !changed.storage.is_empty() + { + let old_info = match >::get(&address) { + Some(ContractInfo::Alive(alive)) => Some(alive), + None => None, + // Cannot commit changes to tombstone contract + Some(ContractInfo::Tombstone(_)) => continue, + }; + + let mut new_info = if let Some(info) = old_info.clone() { info + } else if let Some(code_hash) = changed.code_hash { + AliveContractInfo:: { + code_hash, + storage_size: >::storage_size_offset(), + trie_id: ::TrieIdGenerator::trie_id(&address), + deduct_block: >::block_number(), + rent_allowance: >::zero(), + } } else { - >::get(&address).unwrap() + // No contract exist and no code_hash provided + continue; }; - if let Some(code) = changed.code { - if let Some(code) = code { - >::insert(&address, code); - } else { - >::remove(&address); - } + if let Some(rent_allowance) = changed.rent_allowance { + new_info.rent_allowance = rent_allowance; + } + + if let Some(code_hash) = changed.code_hash { + new_info.code_hash = code_hash; } - let mut new_storage_size = info.storage_size; for (k, v) in changed.storage.into_iter() { - if let Some(value) = child::get_raw(&info.trie_id[..], &blake2_256(&k)) { - new_storage_size -= value.len() as u64; + if let Some(value) = child::get_raw(&new_info.trie_id[..], &blake2_256(&k)) { + new_info.storage_size -= value.len() as u64; } if let Some(value) = v { - new_storage_size += value.len() as u64; - child::put_raw(&info.trie_id[..], &blake2_256(&k), &value[..]); + new_info.storage_size += value.len() as u64; + child::put_raw(&new_info.trie_id[..], &blake2_256(&k), &value[..]); } else { - child::kill(&info.trie_id[..], &blake2_256(&k)); + child::kill(&new_info.trie_id[..], &blake2_256(&k)); } } - if new_storage_size != info.storage_size { - info.storage_size = new_storage_size; - >::insert(&address, info); + if old_info + .map(|old_info| old_info != new_info) + .unwrap_or(true) + { + >::insert(&address, ContractInfo::Alive(new_info)); } } } @@ -140,9 +175,7 @@ pub struct OverlayAccountDb<'a, T: Trait + 'a> { underlying: &'a AccountDb, } impl<'a, T: Trait> OverlayAccountDb<'a, T> { - pub fn new( - underlying: &'a AccountDb, - ) -> OverlayAccountDb<'a, T> { + pub fn new(underlying: &'a AccountDb) -> OverlayAccountDb<'a, T> { OverlayAccountDb { local: RefCell::new(ChangeSet::new()), underlying, @@ -166,12 +199,31 @@ impl<'a, T: Trait> OverlayAccountDb<'a, T> { .insert(location, value); } - pub fn set_code(&mut self, account: &T::AccountId, code: Option>) { + /// Return an error if contract already exists (either if it is alive or tombstone) + pub fn create_contract( + &mut self, + account: &T::AccountId, + code_hash: CodeHash, + ) -> Result<(), &'static str> { + if self.contract_exists(account) { + return Err("Alive contract or tombstone already exists"); + } + + let mut local = self.local.borrow_mut(); + let contract = local.entry(account.clone()).or_insert_with(|| Default::default()); + + contract.code_hash = Some(code_hash); + contract.rent_allowance = Some(>::zero()); + + Ok(()) + } + /// Assume contract exists + pub fn set_rent_allowance(&mut self, account: &T::AccountId, rent_allowance: BalanceOf) { self.local .borrow_mut() .entry(account.clone()) .or_insert(Default::default()) - .code = Some(code); + .rent_allowance = Some(rent_allowance); } pub fn set_balance(&mut self, account: &T::AccountId, balance: BalanceOf) { self.local @@ -191,12 +243,26 @@ impl<'a, T: Trait> AccountDb for OverlayAccountDb<'a, T> { .cloned() .unwrap_or_else(|| self.underlying.get_storage(account, trie_id, location)) } - fn get_code(&self, account: &T::AccountId) -> Option> { + fn get_code_hash(&self, account: &T::AccountId) -> Option> { + self.local + .borrow() + .get(account) + .and_then(|changes| changes.code_hash) + .or_else(|| self.underlying.get_code_hash(account)) + } + fn get_rent_allowance(&self, account: &T::AccountId) -> Option> { + self.local + .borrow() + .get(account) + .and_then(|changes| changes.rent_allowance) + .or_else(|| self.underlying.get_rent_allowance(account)) + } + fn contract_exists(&self, account: &T::AccountId) -> bool { self.local .borrow() .get(account) - .and_then(|a| a.code.clone()) - .unwrap_or_else(|| self.underlying.get_code(account)) + .map(|a| a.code_hash.is_some()) + .unwrap_or_else(|| self.underlying.contract_exists(account)) } fn get_balance(&self, account: &T::AccountId) -> BalanceOf { self.local @@ -212,12 +278,9 @@ impl<'a, T: Trait> AccountDb for OverlayAccountDb<'a, T> { match local.entry(address) { Entry::Occupied(e) => { let mut value = e.into_mut(); - if changed.balance.is_some() { - value.balance = changed.balance; - } - if changed.code.is_some() { - value.code = changed.code; - } + value.balance = changed.balance.or(value.balance); + value.code_hash = changed.code_hash.or(value.code_hash); + value.rent_allowance = changed.rent_allowance.or(value.rent_allowance); value.storage.extend(changed.storage.into_iter()); } Entry::Vacant(e) => { diff --git a/srml/contract/src/exec.rs b/srml/contract/src/exec.rs index 4e3d34c30fb8a..980fd077e9a97 100644 --- a/srml/contract/src/exec.rs +++ b/srml/contract/src/exec.rs @@ -14,7 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -use super::{CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait, TrieId, BalanceOf, AccountInfoOf}; +use super::{CodeHash, Config, ContractAddressFor, Event, RawEvent, Trait, + TrieId, BalanceOf, ContractInfoOf}; use crate::account_db::{AccountDb, DirectAccountDb, OverlayAccountDb}; use crate::gas::{GasMeter, Token, approx_gas_for_balance}; @@ -107,6 +108,12 @@ pub trait Ext { /// Deposit an event. fn deposit_event(&mut self, data: Vec); + + /// Set rent allowance of the contract + fn set_rent_allowance(&mut self, rent_allowance: BalanceOf); + + /// Rent allowance of the contract + fn rent_allowance(&self) -> BalanceOf; } /// Loader is a companion of the `Vm` trait. It loads an appropriate abstract @@ -249,7 +256,8 @@ where /// The specified `origin` address will be used as `sender` for pub fn top_level(origin: T::AccountId, cfg: &'a Config, vm: &'a V, loader: &'a L) -> Self { ExecutionContext { - self_trie_id: >::get(&origin).map(|s| s.trie_id), + self_trie_id: >::get(&origin) + .and_then(|i| i.as_alive().map(|i| i.trie_id.clone())), self_account: origin, overlay: OverlayAccountDb::::new(&DirectAccountDb), depth: 0, @@ -263,7 +271,8 @@ where fn nested(&self, overlay: OverlayAccountDb<'a, T>, dest: T::AccountId) -> Self { ExecutionContext { - self_trie_id: >::get(&dest).map(|s| s.trie_id), + self_trie_id: >::get(&dest) + .and_then(|i| i.as_alive().map(|i| i.trie_id.clone())), self_account: dest, overlay, depth: self.depth + 1, @@ -295,7 +304,11 @@ where return Err("not enough gas to pay base call fee"); } - let dest_code_hash = self.overlay.get_code(&dest); + // Assumption: pay_rent doesn't collide with overlay because + // pay_rent will be done on first call and dest contract and balance + // cannot be changed before the first call + crate::rent::pay_rent::(&dest); + let mut output_data = Vec::new(); let (change_set, events, calls) = { @@ -315,7 +328,7 @@ where )?; } - if let Some(dest_code_hash) = dest_code_hash { + if let Some(dest_code_hash) = self.overlay.get_code_hash(&dest) { let executable = self.loader.load_main(&dest_code_hash)?; output_data = self .vm @@ -369,15 +382,11 @@ where &self.self_account, ); - if self.overlay.get_code(&dest).is_some() { - // It should be enough to check only the code. - return Err("contract already exists"); - } - let (change_set, events, calls) = { let mut overlay = OverlayAccountDb::new(&self.overlay); - - overlay.set_code(&dest, Some(code_hash.clone())); + + overlay.create_contract(&dest, code_hash.clone())?; + let mut nested = self.nested(overlay, dest.clone()); // Send funds unconditionally here. If the `endowment` is below existential_deposit @@ -625,6 +634,15 @@ where fn deposit_event(&mut self, data: Vec) { self.ctx.events.push(RawEvent::Contract(self.ctx.self_account.clone(), data)); } + + fn set_rent_allowance(&mut self, rent_allowance: BalanceOf) { + self.ctx.overlay.set_rent_allowance(&self.ctx.self_account, rent_allowance) + } + + fn rent_allowance(&self) -> BalanceOf { + self.ctx.overlay.get_rent_allowance(&self.ctx.self_account) + .unwrap_or(>::zero()) // Must never be triggered actually + } } /// These tests exercise the executive layer. @@ -763,7 +781,7 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); - ctx.overlay.set_code(&BOB, Some(exec_ch)); + ctx.overlay.create_contract(&BOB, exec_ch).unwrap(); assert_matches!( ctx.call(BOB, value, &mut gas_meter, &data, EmptyOutputBuf::new()), @@ -998,7 +1016,7 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(origin, &cfg, &vm, &loader); - ctx.overlay.set_code(&BOB, Some(return_ch)); + ctx.overlay.create_contract(&BOB, return_ch).unwrap(); let result = ctx.call( dest, @@ -1026,7 +1044,7 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); - ctx.overlay.set_code(&BOB, Some(input_data_ch)); + ctx.overlay.create_contract(&BOB, input_data_ch).unwrap(); let result = ctx.call( BOB, @@ -1038,7 +1056,7 @@ mod tests { assert_matches!(result, Ok(_)); }); - // This one tests passing the input data into a contract via call. + // This one tests passing the input data into a contract via instantiate. with_externalities(&mut ExtBuilder::default().build(), || { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); @@ -1085,7 +1103,7 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); - ctx.overlay.set_code(&BOB, Some(recurse_ch)); + ctx.overlay.create_contract(&BOB, recurse_ch).unwrap(); let result = ctx.call( BOB, @@ -1132,8 +1150,8 @@ mod tests { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(origin, &cfg, &vm, &loader); - ctx.overlay.set_code(&dest, Some(bob_ch)); - ctx.overlay.set_code(&CHARLIE, Some(charlie_ch)); + ctx.overlay.create_contract(&dest, bob_ch).unwrap(); + ctx.overlay.create_contract(&CHARLIE, charlie_ch).unwrap(); let result = ctx.call( dest, @@ -1175,8 +1193,8 @@ mod tests { with_externalities(&mut ExtBuilder::default().build(), || { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); - ctx.overlay.set_code(&BOB, Some(bob_ch)); - ctx.overlay.set_code(&CHARLIE, Some(charlie_ch)); + ctx.overlay.create_contract(&BOB, bob_ch).unwrap(); + ctx.overlay.create_contract(&CHARLIE, charlie_ch).unwrap(); let result = ctx.call( BOB, @@ -1242,7 +1260,7 @@ mod tests { // Check that the newly created account has the expected code hash and // there are instantiation event. - assert_eq!(ctx.overlay.get_code(&created_contract_address).unwrap(), dummy_ch); + assert_eq!(ctx.overlay.get_code_hash(&created_contract_address).unwrap(), dummy_ch); assert_eq!(&ctx.events, &[ RawEvent::Transfer(ALICE, created_contract_address, 100), RawEvent::Instantiated(ALICE, created_contract_address), @@ -1283,7 +1301,7 @@ mod tests { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); ctx.overlay.set_balance(&ALICE, 1000); - ctx.overlay.set_code(&BOB, Some(creator_ch)); + ctx.overlay.create_contract(&BOB, creator_ch).unwrap(); assert_matches!( ctx.call(BOB, 20, &mut GasMeter::::with_limit(1000, 1), &[], EmptyOutputBuf::new()), @@ -1294,7 +1312,7 @@ mod tests { // Check that the newly created account has the expected code hash and // there are instantiation event. - assert_eq!(ctx.overlay.get_code(&created_contract_address).unwrap(), dummy_ch); + assert_eq!(ctx.overlay.get_code_hash(&created_contract_address).unwrap(), dummy_ch); assert_eq!(&ctx.events, &[ RawEvent::Transfer(ALICE, BOB, 20), RawEvent::Transfer(BOB, created_contract_address, 15), @@ -1334,7 +1352,7 @@ mod tests { let cfg = Config::preload(); let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); ctx.overlay.set_balance(&ALICE, 1000); - ctx.overlay.set_code(&BOB, Some(creator_ch)); + ctx.overlay.create_contract(&BOB, creator_ch).unwrap(); assert_matches!( ctx.call(BOB, 20, &mut GasMeter::::with_limit(1000, 1), &[], EmptyOutputBuf::new()), @@ -1349,4 +1367,29 @@ mod tests { } ); } + + #[test] + fn rent_allowance() { + let vm = MockVm::new(); + let mut loader = MockLoader::empty(); + let rent_allowance_ch = loader.insert(|ctx| { + assert_eq!(ctx.ext.rent_allowance(), 0); + ctx.ext.set_rent_allowance(10); + assert_eq!(ctx.ext.rent_allowance(), 10); + VmExecResult::Ok + }); + + with_externalities(&mut ExtBuilder::default().build(), || { + let cfg = Config::preload(); + let mut ctx = ExecutionContext::top_level(ALICE, &cfg, &vm, &loader); + + let result = ctx.instantiate( + 0, + &mut GasMeter::::with_limit(10000, 1), + &rent_allowance_ch, + &[], + ); + assert_matches!(result, Ok(_)); + }); + } } diff --git a/srml/contract/src/lib.rs b/srml/contract/src/lib.rs index e9b218e097ebf..8b89652208165 100644 --- a/srml/contract/src/lib.rs +++ b/srml/contract/src/lib.rs @@ -83,6 +83,7 @@ mod gas; mod account_db; mod exec; mod wasm; +mod rent; #[cfg(test)] mod tests; @@ -96,7 +97,7 @@ use substrate_primitives::crypto::UncheckedFrom; use rstd::prelude::*; use rstd::marker::PhantomData; use parity_codec::{Codec, Encode, Decode}; -use runtime_primitives::traits::{Hash, As, SimpleArithmetic,Bounded, StaticLookup}; +use runtime_primitives::traits::{Hash, As, SimpleArithmetic, Bounded, StaticLookup, Zero}; use srml_support::dispatch::{Result, Dispatchable}; use srml_support::{Parameter, StorageMap, StorageValue, decl_module, decl_event, decl_storage, storage::child}; use srml_support::traits::{OnFreeBalanceZero, OnUnbalanced, Currency}; @@ -117,14 +118,94 @@ pub trait ComputeDispatchFee { fn compute_dispatch_fee(call: &Call) -> Balance; } -#[derive(Encode,Decode,Clone,Debug)] +/// Information for managing an acocunt and its sub trie abstraction. +/// This is the required info to cache for an account +#[derive(Encode, Decode)] +pub enum ContractInfo { + Alive(AliveContractInfo), + Tombstone(TombstoneContractInfo), +} + +impl ContractInfo { + /// If contract is alive then return some alive info + pub fn get_alive(self) -> Option> { + if let ContractInfo::Alive(alive) = self { + Some(alive) + } else { + None + } + } + /// If contract is alive then return some reference to alive info + pub fn as_alive(&self) -> Option<&AliveContractInfo> { + if let ContractInfo::Alive(ref alive) = self { + Some(alive) + } else { + None + } + } + /// If contract is alive then return some mutable reference to alive info + pub fn as_alive_mut(&mut self) -> Option<&mut AliveContractInfo> { + if let ContractInfo::Alive(ref mut alive) = self { + Some(alive) + } else { + None + } + } + + /// If contract is tombstone then return some alive info + pub fn get_tombstone(self) -> Option> { + if let ContractInfo::Tombstone(tombstone) = self { + Some(tombstone) + } else { + None + } + } + /// If contract is tombstone then return some reference to tombstone info + pub fn as_tombstone(&self) -> Option<&TombstoneContractInfo> { + if let ContractInfo::Tombstone(ref tombstone) = self { + Some(tombstone) + } else { + None + } + } + /// If contract is tombstone then return some mutable reference to tombstone info + pub fn as_tombstone_mut(&mut self) -> Option<&mut TombstoneContractInfo> { + if let ContractInfo::Tombstone(ref mut tombstone) = self { + Some(tombstone) + } else { + None + } + } +} + +pub type AliveContractInfo = RawAliveContractInfo, BalanceOf, ::BlockNumber>; + /// Information for managing an account and its sub trie abstraction. /// This is the required info to cache for an account. -pub struct AccountInfo { - /// Unique ID for the subtree encoded as a byte. +// Workaround for https://github.com/rust-lang/rust/issues/26925 . Remove when sorted. +#[derive(Encode, Decode, Clone, PartialEq, Eq)] +pub struct RawAliveContractInfo { + /// Unique ID for the subtree encoded as a bytes vector. pub trie_id: TrieId, /// The size of stored value in octet. pub storage_size: u64, + /// The code associated with a given account. + pub code_hash: CodeHash, + pub rent_allowance: Balance, + pub deduct_block: BlockNumber, +} + +#[derive(Encode, Decode)] +pub struct TombstoneContractInfo(T::Hash); + +impl TombstoneContractInfo { + fn new(storage_root: Vec, storage_size: u64, code_hash: CodeHash) -> Self { + let mut buf = Vec::new(); + storage_root.using_encoded(|encoded| buf.extend_from_slice(encoded)); + storage_size.using_encoded(|encoded| buf.extend_from_slice(encoded)); + buf.extend_from_slice(code_hash.as_ref()); + TombstoneContractInfo(T::Hashing::hash(&buf[..])) + } } /// Get a trie id (trie id must be unique and collision resistant depending upon its context). @@ -384,6 +465,39 @@ decl_module! { result.map(|_| ()) } + /// Allows block producers to claim a small reward for evicting a contract. If a block producer + /// fails to do so, a regular users will be allowed to claim the reward. + /// + /// If contract is not evicted as a result of this call, no actions are taken and + /// the sender is not eligible for the reward. + fn claim_surcharge(origin, dest: T::AccountId, aux_sender: Option) { + let origin = origin.into(); + let (signed, rewarded) = match origin { + Some(system::RawOrigin::Signed(ref account)) if aux_sender.is_none() => { + (true, account) + }, + Some(system::RawOrigin::Inherent) if aux_sender.is_some() => { + (false, aux_sender.as_ref().expect("checked above")) + }, + _ => return Err("Invalid surcharge claim: origin must be signed or \ + inherent and auxiliary sender only provided on inherent") + }; + + // Add some advantage for block producers (who send unsigned extrinsics) by + // adding a handicap: for signed extrinsics we use a slightly older block number + // for the eviction check. This can be viewed as if we pushed regular users back in past. + let handicap = if signed { + >::signed_claim_handicap() + } else { + Zero::zero() + }; + + // If poking the contract has lead to eviction of the contract, give out the rewards. + if rent::try_evict::(&dest, handicap) == rent::RentOutcome::Evicted { + T::Currency::deposit_into_existing(rewarded, Self::surcharge_reward())?; + } + } + fn on_finalize() { >::kill(); } @@ -420,6 +534,29 @@ decl_event! { decl_storage! { trait Store for Module as Contract { + /// Number of block delay an extrinsic claim surcharge has. + /// + /// When claim surchage is called by an extrinsic the rent is checked + /// for current_block - delay + SignedClaimHandicap get(signed_claim_handicap) config(): T::BlockNumber; + /// The minimum amount required to generate a tombstone. + TombstoneDeposit get(tombstone_deposit) config(): BalanceOf; + /// Size of a contract at the time of creation. This is a simple way to ensure + /// that empty contracts eventually gets deleted. + StorageSizeOffset get(storage_size_offset) config(): u64; + /// Price of a byte of storage per one block interval. Should be greater than 0. + RentByteFee get(rent_byte_price) config(): BalanceOf; + /// The amount of funds a contract should deposit in order to offset + /// the cost of one byte. + /// + /// Let's suppose the deposit is 1,000 EDG/byte and the rent is 1 EDG/byte/day, then a contract + /// with 1,000,000 EDG that uses 1,000 bytes of storage would pay no rent. + /// But if the balance reduced to 500,000 EDG and the storage stayed the same at 1,000, + /// then it would pay 500 EDG/day. + RentDepositOffset get(rent_deposit_offset) config(): BalanceOf; + /// Reward that is received by the party whose touch has led + /// to removal of a contract. + SurchargeReward get(surcharge_reward) config(): BalanceOf; /// The fee required to make a transfer. TransferFee get(transfer_fee) config(): BalanceOf; /// The fee required to create an account. @@ -444,8 +581,6 @@ decl_storage! { GasSpent get(gas_spent): T::Gas; /// Current cost schedule for contracts. CurrentSchedule get(current_schedule) config(): Schedule = Schedule::default(); - /// The code associated with a given account. - pub CodeHashOf: map T::AccountId => Option>; /// A mapping from an original code hash to the original code, untouched by instrumentation. pub PristineCode: map CodeHash => Option>; /// A mapping between an original code hash and instrumented wasm code, ready for execution. @@ -453,14 +588,16 @@ decl_storage! { /// The subtrie counter. pub AccountCounter: u64 = 0; /// The code associated with a given account. - pub AccountInfoOf: map T::AccountId => Option; + pub ContractInfoOf: map T::AccountId => Option>; } } impl OnFreeBalanceZero for Module { fn on_free_balance_zero(who: &T::AccountId) { - >::remove(who); - >::get(who).map(|info| child::kill_storage(&info.trie_id)); + if let Some(ContractInfo::Alive(info)) = >::get(who) { + child::kill_storage(&info.trie_id); + } + >::remove(who); } } diff --git a/srml/contract/src/rent.rs b/srml/contract/src/rent.rs new file mode 100644 index 0000000000000..b914e41af3198 --- /dev/null +++ b/srml/contract/src/rent.rs @@ -0,0 +1,190 @@ +// Copyright 2019 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 . + +use crate::{BalanceOf, ContractInfo, ContractInfoOf, Module, TombstoneContractInfo, Trait}; +use runtime_primitives::traits::{As, Bounded, CheckedDiv, CheckedMul, Saturating, Zero}; +use srml_support::traits::{Currency, ExistenceRequirement, Imbalance, WithdrawReason}; +use srml_support::StorageMap; + +#[derive(PartialEq, Eq, Copy, Clone)] +#[must_use] +pub enum RentOutcome { + /// Exempted from rent iff: + /// * rent is offset completely by the `rent_deposit_offset`, + /// * or rent has already been paid for this block number, + /// * or account doesn't have a contract, + /// * or account has a tombstone. + Exempted, + /// Evicted iff: + /// * rent exceed rent allowance, + /// * or can't withdraw the rent, + /// * or go below subsistence threshold. + Evicted, + /// The outstanding dues were paid or were able to be paid. + Ok, +} + +/// Evict and optionally pay dues (or check account can pay them otherwise) at the current +/// block number (modulo `handicap`, read on). +/// +/// `pay_rent` gives an ability to pay or skip paying rent. +/// `handicap` gives a way to check or pay the rent up to a moment in the past instead +/// of current block. +/// +/// NOTE: This function acts eagerly, all modification are committed into the storage. +fn try_evict_or_and_pay_rent( + account: &T::AccountId, + handicap: T::BlockNumber, + pay_rent: bool, +) -> RentOutcome { + let contract = match >::get(account) { + None | Some(ContractInfo::Tombstone(_)) => return RentOutcome::Exempted, + Some(ContractInfo::Alive(contract)) => contract, + }; + + // How much block has passed since the last deduction for the contract. + let blocks_passed = { + // Calculate an effective block number, i.e. after adjusting for handicap. + let effective_block_number = >::block_number().saturating_sub(handicap); + let n = effective_block_number.saturating_sub(contract.deduct_block); + if n.is_zero() { + // Rent has already been paid + return RentOutcome::Exempted; + } + n + }; + + let balance = T::Currency::free_balance(account); + + // An amount of funds to charge per block for storage taken up by the contract. + let fee_per_block = { + let free_storage = balance + .checked_div(&>::rent_deposit_offset()) + .unwrap_or(>::sa(0)); + + let effective_storage_size = + >::sa(contract.storage_size).saturating_sub(free_storage); + + effective_storage_size + .checked_mul(&>::rent_byte_price()) + .unwrap_or(>::max_value()) + }; + + if fee_per_block.is_zero() { + // The rent deposit offset reduced the fee to 0. This means that the contract + // gets the rent for free. + return RentOutcome::Exempted; + } + + // The minimal amount of funds required for a contract not to be evicted. + let subsistence_threshold = T::Currency::minimum_balance() + >::tombstone_deposit(); + + let dues = fee_per_block + .checked_mul(&>::sa(blocks_passed.as_())) + .unwrap_or(>::max_value()); + + let dues_limited = dues.min(contract.rent_allowance); + let rent_allowance_exceeded = dues > contract.rent_allowance; + let is_below_subsistence = balance < subsistence_threshold; + let go_below_subsistence = balance.saturating_sub(dues_limited) < subsistence_threshold; + let can_withdraw_rent = T::Currency::ensure_can_withdraw( + account, + dues_limited, + WithdrawReason::Fee, + balance.saturating_sub(dues_limited), + ) + .is_ok(); + + if !rent_allowance_exceeded && can_withdraw_rent && !go_below_subsistence { + // Collect dues + + if pay_rent { + let imbalance = T::Currency::withdraw( + account, + dues, + WithdrawReason::Fee, + ExistenceRequirement::KeepAlive, + ) + .expect( + "Withdraw has been checked above; + go_below_subsistence is false and subsistence > existencial_deposit; + qed", + ); + + >::mutate(account, |contract| { + contract + .as_mut() + .and_then(|c| c.as_alive_mut()) + .expect("Dead or inexistent account has been exempt above; qed") + .rent_allowance -= imbalance.peek(); // rent_allowance is not exceeded + }) + } + + RentOutcome::Ok + } else { + // Evict + + if can_withdraw_rent && !go_below_subsistence { + T::Currency::withdraw( + account, + dues, + WithdrawReason::Fee, + ExistenceRequirement::KeepAlive, + ) + .expect("Can withdraw and don't go below subsistence"); + } else if !is_below_subsistence { + T::Currency::make_free_balance_be(account, subsistence_threshold); + } else { + T::Currency::make_free_balance_be(account, >::zero()); + } + + if !is_below_subsistence { + // The contract has funds above subsistence deposit and that means it can afford to + // leave tombstone. + + // Note: this operation is heavy. + let child_storage_root = runtime_io::child_storage_root(&contract.trie_id); + + let tombstone = TombstoneContractInfo::new( + child_storage_root, + contract.storage_size, + contract.code_hash, + ); + >::insert(account, ContractInfo::Tombstone(tombstone)); + runtime_io::kill_child_storage(&contract.trie_id); + } + + RentOutcome::Evicted + } +} + +/// Make account paying the rent for the current block number +/// +/// NOTE: This function acts eagerly. +pub fn pay_rent(account: &T::AccountId) { + let _ = try_evict_or_and_pay_rent::(account, Zero::zero(), true); +} + +/// Evict the account if it should be evicted at the given block number. +/// +/// `handicap` gives a way to check or pay the rent up to a moment in the past instead +/// of current block. E.g. if the contract is going to be evicted at the current block, +/// `handicap=1` can defer the eviction for 1 block. +/// +/// NOTE: This function acts eagerly. +pub fn try_evict(account: &T::AccountId, handicap: T::BlockNumber) -> RentOutcome { + try_evict_or_and_pay_rent::(account, handicap, false) +} diff --git a/srml/contract/src/tests.rs b/srml/contract/src/tests.rs index 0e7270769c085..e18a99b8327bb 100644 --- a/srml/contract/src/tests.rs +++ b/srml/contract/src/tests.rs @@ -19,27 +19,28 @@ #![allow(unused)] +use crate::account_db::{AccountDb, DirectAccountDb, OverlayAccountDb}; +use crate::{ + ComputeDispatchFee, ContractAddressFor, ContractInfo, ContractInfoOf, GenesisConfig, Module, + RawAliveContractInfo, RawEvent, Trait, TrieId, TrieIdFromParentCounter, TrieIdGenerator, +}; +use assert_matches::assert_matches; +use hex_literal::*; +use parity_codec::{Decode, Encode, KeyedVec}; +use runtime_io; use runtime_io::with_externalities; -use runtime_primitives::testing::{Digest, DigestItem, H256, Header, UintAuthorityId}; -use runtime_primitives::traits::{BlakeTwo256, IdentityLookup}; +use runtime_primitives::testing::{Digest, DigestItem, Header, UintAuthorityId, H256}; +use runtime_primitives::traits::{As, BlakeTwo256, IdentityLookup}; use runtime_primitives::BuildStorage; -use runtime_io; -use srml_support::{storage::child, StorageMap, assert_ok, impl_outer_event, impl_outer_dispatch, - impl_outer_origin, traits::Currency}; -use substrate_primitives::Blake2Hasher; -use system::{self, Phase, EventRecord}; -use {wabt, balances, consensus}; -use hex_literal::hex; -use assert_matches::assert_matches; -use crate::{ - ContractAddressFor, GenesisConfig, Module, RawEvent, - Trait, ComputeDispatchFee, TrieIdGenerator, TrieId, - AccountInfo, AccountInfoOf, TrieIdFromParentCounter +use srml_support::{ + assert_ok, impl_outer_dispatch, impl_outer_event, impl_outer_origin, storage::child, + traits::Currency, StorageMap, }; -use substrate_primitives::storage::well_known_keys; -use parity_codec::{Encode, Decode, KeyedVec}; use std::sync::atomic::{AtomicUsize, Ordering}; -use crate::account_db::{DirectAccountDb, OverlayAccountDb, AccountDb}; +use substrate_primitives::storage::well_known_keys; +use substrate_primitives::Blake2Hasher; +use system::{self, EventRecord, Phase}; +use {balances, consensus, wabt}; mod contract { // Re-export contents of the root. This basically @@ -206,8 +207,14 @@ impl ExtBuilder { ); t.extend( GenesisConfig:: { - transaction_base_fee: 0, - transaction_byte_fee: 0, + signed_claim_handicap: 2, + rent_byte_price: 4, + rent_deposit_offset: 10_000, + storage_size_offset: 8, + surcharge_reward: 150, + tombstone_deposit: 16, + transaction_base_fee: 2, + transaction_byte_fee: 6, transfer_fee: self.transfer_fee, creation_fee: self.creation_fee, contract_fee: 21, @@ -231,13 +238,7 @@ fn refunds_unused_gas() { with_externalities(&mut ExtBuilder::default().build(), || { Balances::deposit_creating(&0, 100_000_000); - assert_ok!(Contract::call( - Origin::signed(0), - 1, - 0, - 100_000, - Vec::new() - )); + assert_ok!(Contract::call(Origin::signed(0), 1, 0, 100_000, Vec::new())); assert_eq!(Balances::free_balance(&0), 100_000_000 - (2 * 135)); }); @@ -256,10 +257,13 @@ fn account_removal_removes_storage() { // Set up two accounts with free balance above the existential threshold. { Balances::deposit_creating(&1, 110); - AccountInfoOf::::insert(1, &AccountInfo { + ContractInfoOf::::insert(1, &ContractInfo::Alive(RawAliveContractInfo { trie_id: trie_id1.clone(), - storage_size: 0, - }); + storage_size: Contract::storage_size_offset(), + deduct_block: System::block_number(), + code_hash: H256::repeat_byte(1), + rent_allowance: 40, + })); let mut overlay = OverlayAccountDb::::new(&DirectAccountDb); overlay.set_storage(&1, key1.clone(), Some(b"1".to_vec())); @@ -267,10 +271,13 @@ fn account_removal_removes_storage() { DirectAccountDb.commit(overlay.into_change_set()); Balances::deposit_creating(&2, 110); - AccountInfoOf::::insert(2, &AccountInfo { + ContractInfoOf::::insert(2, &ContractInfo::Alive(RawAliveContractInfo { trie_id: trie_id2.clone(), - storage_size: 0, - }); + storage_size: Contract::storage_size_offset(), + deduct_block: System::block_number(), + code_hash: H256::repeat_byte(2), + rent_allowance: 40, + })); let mut overlay = OverlayAccountDb::::new(&DirectAccountDb); overlay.set_storage(&2, key1.clone(), Some(b"3".to_vec())); @@ -287,7 +294,6 @@ fn account_removal_removes_storage() { // Verify that all entries from account 1 is removed, while // entries from account 2 is in place. { - // let a: ::AccountId = 1; assert!(>::get_storage(&DirectAccountDb, &1, Some(&trie_id1), key1).is_none()); assert!(>::get_storage(&DirectAccountDb, &1, Some(&trie_id1), key2).is_none()); @@ -342,11 +348,7 @@ fn instantiate_and_call_and_deposit_event() { || { Balances::deposit_creating(&ALICE, 1_000_000); - assert_ok!(Contract::put_code( - Origin::signed(ALICE), - 100_000, - wasm, - )); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm)); // Check at the end to get hash on error easily let creation = Contract::create( @@ -387,7 +389,7 @@ fn instantiate_and_call_and_deposit_event() { ]); assert_ok!(creation); - assert!(AccountInfoOf::::exists(BOB)); + assert!(ContractInfoOf::::exists(BOB)); }, ); } @@ -424,11 +426,7 @@ fn dispatch_call() { || { Balances::deposit_creating(&ALICE, 1_000_000); - assert_ok!(Contract::put_code( - Origin::signed(ALICE), - 100_000, - wasm, - )); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm)); // Let's keep this assert even though it's redundant. If you ever need to update the // wasm source this test will fail and will show you the actual hash. @@ -506,3 +504,399 @@ fn dispatch_call() { }, ); } + +const CODE_SET_RENT: &str = r#" +(module + (import "env" "ext_dispatch_call" (func $ext_dispatch_call (param i32 i32))) + (import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32 i32))) + (import "env" "ext_set_rent_allowance" (func $ext_set_rent_allowance (param i32 i32))) + (import "env" "ext_input_size" (func $ext_input_size (result i32))) + (import "env" "ext_input_copy" (func $ext_input_copy (param i32 i32 i32))) + (import "env" "memory" (memory 1 1)) + + ;; insert a value of 4 bytes into storage + (func $call_0 + (call $ext_set_storage + (i32.const 1) + (i32.const 1) + (i32.const 0) + (i32.const 4) + ) + ) + + ;; remove the value inserted by call_1 + (func $call_1 + (call $ext_set_storage + (i32.const 1) + (i32.const 0) + (i32.const 0) + (i32.const 0) + ) + ) + + ;; transfer 50 to ALICE + (func $call_2 + (call $ext_dispatch_call + (i32.const 8) + (i32.const 11) + ) + ) + + ;; do nothing + (func $call_else) + + (func $assert (param i32) + (block $ok + (br_if $ok + (get_local 0) + ) + (unreachable) + ) + ) + + ;; Dispatch the call according to input size + (func (export "call") + (local $input_size i32) + (set_local $input_size + (call $ext_input_size) + ) + (block $IF_ELSE + (block $IF_2 + (block $IF_1 + (block $IF_0 + (br_table $IF_0 $IF_1 $IF_2 $IF_ELSE + (get_local $input_size) + ) + (unreachable) + ) + (call $call_0) + return + ) + (call $call_1) + return + ) + (call $call_2) + return + ) + (call $call_else) + ) + + ;; Set into storage a 4 bytes value + ;; Set call set_rent_allowance with input + (func (export "deploy") + (local $input_size i32) + (call $ext_set_storage + (i32.const 0) + (i32.const 1) + (i32.const 0) + (i32.const 4) + ) + (set_local $input_size + (call $ext_input_size) + ) + (call $ext_input_copy + (i32.const 0) + (i32.const 0) + (get_local $input_size) + ) + (call $ext_set_rent_allowance + (i32.const 0) + (get_local $input_size) + ) + ) + + ;; Encoding of 10 in balance + (data (i32.const 0) "\28") + + ;; Encoding of call transfer 50 to CHARLIE + (data (i32.const 8) "\00\00\03\00\00\00\00\00\00\00\C8") +) +"#; +const HASH_SET_RENT: [u8; 32] = hex!("a51c2a6f3f68936d4ae9abdb93b28eedcbd0f6f39770e168f9025f0c1e7094ef"); + + +/// Input data for each call in set_rent code +mod call { + pub fn set_storage_4_byte() -> Vec { vec![] } + pub fn remove_storage_4_byte() -> Vec { vec![0] } + pub fn transfer() -> Vec { vec![0, 0] } + pub fn null() -> Vec { vec![0, 0, 0] } +} + +/// Test correspondance of set_rent code and its hash. +/// Also test that encoded extrinsic in code correspond to the correct transfer +#[test] +fn set_rent_hash_and_code() { + // This test can fail due to the encoding changes. In case it becomes too annoying + // let's rewrite so as we use this module controlled call or we serialize it in runtime. + let encoded = parity_codec::Encode::encode(&Call::Balances(balances::Call::transfer(CHARLIE, 50))); + assert_eq!(&encoded[..], &hex!("00000300000000000000C8")[..]); + + let wasm = wabt::wat2wasm(CODE_SET_RENT).unwrap(); + + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm)); + + // If you ever need to update the wasm source this test will fail and will show you the actual hash. + assert_eq!(System::events(), vec![ + EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: MetaEvent::balances(balances::RawEvent::NewAccount(1, 1_000_000)), + }, + EventRecord { + phase: Phase::ApplyExtrinsic(0), + event: MetaEvent::contract(RawEvent::CodeStored(HASH_SET_RENT.into())), + }, + ]); + } + ); +} + +#[test] +fn storage_size() { + let wasm = wabt::wat2wasm(CODE_SET_RENT).unwrap(); + + // Storage size + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + // Create + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm)); + assert_ok!(Contract::create( + Origin::signed(ALICE), + 30_000, + 100_000, HASH_SET_RENT.into(), + ::Balance::sa(1_000u64).encode() // rent allowance + )); + let bob_contract = super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap(); + assert_eq!(bob_contract.storage_size, Contract::storage_size_offset() + 4); + + assert_ok!(Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::set_storage_4_byte())); + let bob_contract = super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap(); + assert_eq!(bob_contract.storage_size, Contract::storage_size_offset() + 4 + 4); + + assert_ok!(Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::remove_storage_4_byte())); + let bob_contract = super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap(); + assert_eq!(bob_contract.storage_size, Contract::storage_size_offset() + 4); + } + ); +} + +#[test] +fn deduct_blocks() { + let wasm = wabt::wat2wasm(CODE_SET_RENT).unwrap(); + + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + // Create + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm)); + assert_ok!(Contract::create( + Origin::signed(ALICE), + 30_000, + 100_000, HASH_SET_RENT.into(), + ::Balance::sa(1_000u64).encode() // rent allowance + )); + + // Check creation + let bob_contract = super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap(); + assert_eq!(bob_contract.rent_allowance, 1_000); + + // Advance 4 blocks + System::initialize(&5, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent through call + assert_ok!(Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null())); + + // Check result + let rent = (8 + 4 - 3) // storage size = size_offset + deploy_set_storage - deposit_offset + * 4 // rent byte price + * 4; // blocks to rent + let bob_contract = super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap(); + assert_eq!(bob_contract.rent_allowance, 1_000 - rent); + assert_eq!(Balances::free_balance(BOB), 30_000 - rent); + } + ); +} + +#[test] +fn call_contract_removals() { + removals(|| Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::null()).is_ok()); +} + +#[test] +fn inherent_claim_surcharge_contract_removals() { + removals(|| Contract::claim_surcharge(Origin::INHERENT, BOB, Some(ALICE)).is_ok()); +} + +#[test] +fn signed_claim_surcharge_contract_removals() { + removals(|| Contract::claim_surcharge(Origin::signed(ALICE), BOB, None).is_ok()); +} + +#[test] +fn claim_surcharge_malus() { + // Test surcharge malus for inherent + claim_surcharge(4, || Contract::claim_surcharge(Origin::INHERENT, BOB, Some(ALICE)).is_ok(), true); + claim_surcharge(3, || Contract::claim_surcharge(Origin::INHERENT, BOB, Some(ALICE)).is_ok(), true); + claim_surcharge(2, || Contract::claim_surcharge(Origin::INHERENT, BOB, Some(ALICE)).is_ok(), true); + claim_surcharge(1, || Contract::claim_surcharge(Origin::INHERENT, BOB, Some(ALICE)).is_ok(), false); + + // Test surcharge malus for signed + claim_surcharge(4, || Contract::claim_surcharge(Origin::signed(ALICE), BOB, None).is_ok(), true); + claim_surcharge(3, || Contract::claim_surcharge(Origin::signed(ALICE), BOB, None).is_ok(), false); + claim_surcharge(2, || Contract::claim_surcharge(Origin::signed(ALICE), BOB, None).is_ok(), false); + claim_surcharge(1, || Contract::claim_surcharge(Origin::signed(ALICE), BOB, None).is_ok(), false); +} + +/// Claim surcharge with the given trigger_call at the given blocks. +/// if removes is true then assert that the contract is a tombstonedead +fn claim_surcharge(blocks: u64, trigger_call: impl Fn() -> bool, removes: bool) { + let wasm = wabt::wat2wasm(CODE_SET_RENT).unwrap(); + + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + // Create + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm)); + assert_ok!(Contract::create( + Origin::signed(ALICE), + 100, + 100_000, HASH_SET_RENT.into(), + ::Balance::sa(1_000u64).encode() // rent allowance + )); + + // Advance blocks + System::initialize(&blocks, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent through call + assert!(trigger_call()); + + if removes { + assert!(super::ContractInfoOf::::get(BOB).unwrap().get_tombstone().is_some()); + } else { + assert!(super::ContractInfoOf::::get(BOB).unwrap().get_alive().is_some()); + } + } + ); +} + +/// Test for all kind of removals for the given trigger: +/// * if balance is reached and balance > subsistence threshold +/// * if allowance is exceeded +/// * if balance is reached and balance < subsistence threshold +fn removals(trigger_call: impl Fn() -> bool) { + let wasm = wabt::wat2wasm(CODE_SET_RENT).unwrap(); + + // Balance reached and superior to subsistence threshold + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + // Create + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm.clone())); + assert_ok!(Contract::create( + Origin::signed(ALICE), + 100, + 100_000, HASH_SET_RENT.into(), + ::Balance::sa(1_000u64).encode() // rent allowance + )); + + // Trigger rent must have no effect + assert!(trigger_call()); + assert_eq!(super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap().rent_allowance, 1_000); + + // Advance blocks + System::initialize(&10, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent through call + assert!(trigger_call()); + assert!(super::ContractInfoOf::::get(BOB).unwrap().get_tombstone().is_some()); + + // Advance blocks + System::initialize(&20, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent must have no effect + assert!(trigger_call()); + assert!(super::ContractInfoOf::::get(BOB).unwrap().get_tombstone().is_some()); + } + ); + + // Allowance exceeded + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + // Create + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm.clone())); + assert_ok!(Contract::create( + Origin::signed(ALICE), + 1_000, + 100_000, HASH_SET_RENT.into(), + ::Balance::sa(100u64).encode() // rent allowance + )); + + // Trigger rent must have no effect + assert!(trigger_call()); + assert_eq!(super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap().rent_allowance, 100); + + // Advance blocks + System::initialize(&10, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent through call + assert!(trigger_call()); + assert!(super::ContractInfoOf::::get(BOB).unwrap().get_tombstone().is_some()); + + // Advance blocks + System::initialize(&20, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent must have no effect + assert!(trigger_call()); + assert!(super::ContractInfoOf::::get(BOB).unwrap().get_tombstone().is_some()); + } + ); + + // Balance reached and inferior to subsistence threshold + with_externalities( + &mut ExtBuilder::default().existential_deposit(50).build(), + || { + // Create + Balances::deposit_creating(&ALICE, 1_000_000); + assert_ok!(Contract::put_code(Origin::signed(ALICE), 100_000, wasm.clone())); + assert_ok!(Contract::create( + Origin::signed(ALICE), + 50+Balances::minimum_balance(), + 100_000, HASH_SET_RENT.into(), + ::Balance::sa(1_000u64).encode() // rent allowance + )); + + // Trigger rent must have no effect + assert!(trigger_call()); + assert_eq!(super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap().rent_allowance, 1_000); + + // Transfer funds + assert_ok!(Contract::call(Origin::signed(ALICE), BOB, 0, 100_000, call::transfer())); + assert_eq!(super::ContractInfoOf::::get(BOB).unwrap().get_alive().unwrap().rent_allowance, 1_000); + + // Advance blocks + System::initialize(&10, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent through call + assert!(trigger_call()); + assert!(super::ContractInfoOf::::get(BOB).is_none()); + + // Advance blocks + System::initialize(&20, &[0u8; 32].into(), &[0u8; 32].into()); + + // Trigger rent must have no effect + assert!(trigger_call()); + assert!(super::ContractInfoOf::::get(BOB).is_none()); + } + ); +} diff --git a/srml/contract/src/wasm/mod.rs b/srml/contract/src/wasm/mod.rs index 6aa7b73f52f28..27b8577221c6d 100644 --- a/srml/contract/src/wasm/mod.rs +++ b/srml/contract/src/wasm/mod.rs @@ -200,6 +200,7 @@ mod tests { #[derive(Default)] pub struct MockExt { storage: HashMap>, + rent_allowance: u64, creates: Vec, transfers: Vec, dispatches: Vec, @@ -281,6 +282,14 @@ mod tests { fn deposit_event(&mut self, data: Vec) { self.events.push(data) } + + fn set_rent_allowance(&mut self, rent_allowance: u64) { + self.rent_allowance = rent_allowance; + } + + fn rent_allowance(&self) -> u64 { + self.rent_allowance + } } fn execute( diff --git a/srml/contract/src/wasm/runtime.rs b/srml/contract/src/wasm/runtime.rs index 1c48d248d2973..3f7b10045c296 100644 --- a/srml/contract/src/wasm/runtime.rs +++ b/srml/contract/src/wasm/runtime.rs @@ -629,6 +629,31 @@ define_env!(Env, , Ok(()) }, + // Set rent allowance of the contract + // + // - value_ptr: a pointer to the buffer with value, how much to allow for rent + // Should be decodable as a `T::Balance`. Traps otherwise. + // - value_len: length of the value buffer. + ext_set_rent_allowance(ctx, value_ptr: u32, value_len: u32) => { + let value = { + let value_buf = read_sandbox_memory(ctx, value_ptr, value_len)?; + BalanceOf::<::T>::decode(&mut &value_buf[..]) + .ok_or_else(|| sandbox::HostError)? + }; + ctx.ext.set_rent_allowance(value); + + Ok(()) + }, + + // Stores the rent allowance into the scratch buffer. + // + // The data is encoded as T::Balance. The current contents of the scratch buffer are overwritten. + ext_rent_allowance(ctx) => { + ctx.scratch_buf = ctx.ext.rent_allowance().encode(); + + Ok(()) + }, + // Prints utf8 encoded string from the data buffer. // Only available on `--dev` chains. // This function may be removed at any time, superseded by a more general contract debugging feature.