diff --git a/substrate/runtime/contract/src/gas.rs b/substrate/runtime/contract/src/gas.rs index 2f2b82d6b0142..b405e31d7fd59 100644 --- a/substrate/runtime/contract/src/gas.rs +++ b/substrate/runtime/contract/src/gas.rs @@ -14,8 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -use {Trait, Module}; +use {Trait, Module, GasSpent}; use runtime_primitives::traits::{As, CheckedMul, CheckedSub, Zero}; +use runtime_support::StorageValue; use staking; #[must_use] @@ -35,6 +36,8 @@ impl GasMeterResult { } pub struct GasMeter { + limit: T::Gas, + /// Amount of gas left from initial gas limit. Can reach zero. gas_left: T::Gas, gas_price: T::Balance, } @@ -42,6 +45,7 @@ impl GasMeter { #[cfg(test)] pub fn with_limit(gas_limit: T::Gas, gas_price: T::Balance) -> GasMeter { GasMeter { + limit: gas_limit, gas_left: gas_limit, gas_price, } @@ -101,6 +105,7 @@ impl GasMeter { } else { self.gas_left = self.gas_left - amount; let mut nested = GasMeter { + limit: amount, gas_left: amount, gas_price: self.gas_price, }; @@ -117,6 +122,11 @@ impl GasMeter { pub fn gas_left(&self) -> T::Gas { self.gas_left } + + /// Returns how much gas was spent. + fn spent(&self) -> T::Gas { + self.limit - self.gas_left + } } /// Buy the given amount of gas. @@ -127,6 +137,14 @@ pub fn buy_gas( transactor: &T::AccountId, gas_limit: T::Gas, ) -> Result, &'static str> { + // Check if the specified amount of gas is available in the current block. + // This cannot underflow since `gas_spent` is never greater than `block_gas_limit`. + let gas_available = >::block_gas_limit() - >::gas_spent(); + if gas_limit > gas_available { + return Err("block gas limit is reached"); + } + + // Buy the specified amount of gas. let gas_price = >::gas_price(); let b = >::free_balance(transactor); let cost = >::as_(gas_limit.clone()) @@ -138,6 +156,7 @@ pub fn buy_gas( >::set_free_balance(transactor, b - cost); >::decrease_total_stake_by(cost); Ok(GasMeter { + limit: gas_limit, gas_left: gas_limit, gas_price, }) @@ -145,6 +164,13 @@ pub fn buy_gas( /// Refund the unused gas. pub fn refund_unused_gas(transactor: &T::AccountId, gas_meter: GasMeter) { + // Increase total spent gas. + // This cannot overflow, since `gas_spent` is never greater than `block_gas_limit`, which + // also has T::Gas type. + let gas_spent = >::gas_spent() + gas_meter.spent(); + >::put(gas_spent); + + // Refund gas left by the price it was bought. let b = >::free_balance(transactor); let refund = >::as_(gas_meter.gas_left) * gas_meter.gas_price; >::set_free_balance(transactor, b + refund); diff --git a/substrate/runtime/contract/src/genesis_config.rs b/substrate/runtime/contract/src/genesis_config.rs index 89f94b40b94ef..08f09324a5ad7 100644 --- a/substrate/runtime/contract/src/genesis_config.rs +++ b/substrate/runtime/contract/src/genesis_config.rs @@ -16,7 +16,7 @@ //! Build the contract module part of the genesis block storage. -use {Trait, ContractFee, CallBaseFee, CreateBaseFee, GasPrice, MaxDepth}; +use {Trait, ContractFee, CallBaseFee, CreateBaseFee, GasPrice, MaxDepth, BlockGasLimit}; use runtime_primitives; use runtime_io::{self, twox_128}; @@ -34,6 +34,7 @@ pub struct GenesisConfig { pub create_base_fee: T::Gas, pub gas_price: T::Balance, pub max_depth: u32, + pub block_gas_limit: T::Gas, } impl runtime_primitives::BuildStorage for GenesisConfig { @@ -43,7 +44,8 @@ impl runtime_primitives::BuildStorage for GenesisConfig { twox_128(>::key()).to_vec() => self.call_base_fee.encode(), twox_128(>::key()).to_vec() => self.create_base_fee.encode(), twox_128(>::key()).to_vec() => self.gas_price.encode(), - twox_128(>::key()).to_vec() => self.max_depth.encode() + twox_128(>::key()).to_vec() => self.max_depth.encode(), + twox_128(>::key()).to_vec() => self.block_gas_limit.encode() ]; Ok(r.into()) } diff --git a/substrate/runtime/contract/src/lib.rs b/substrate/runtime/contract/src/lib.rs index bdeebcf7b29fd..814e945ea3102 100644 --- a/substrate/runtime/contract/src/lib.rs +++ b/substrate/runtime/contract/src/lib.rs @@ -25,13 +25,31 @@ //! create smart-contracts or send messages to existing smart-contracts. //! //! For any actions invoked by the smart-contracts fee must be paid. The fee is paid in gas. -//! Gas is bought upfront. Any unused is refunded after the transaction (regardless of the -//! execution outcome). If all gas is used, then changes made for the specific call or create -//! are reverted (including balance transfers). +//! Gas is bought upfront up to the, specified in transaction, limit. Any unused gas is refunded +//! after the transaction (regardless of the execution outcome). If all gas is used, +//! then changes made for the specific call or create are reverted (including balance transfers). //! //! Failures are typically not cascading. That, for example, means that if contract A calls B and B errors //! somehow, then A can decide if it should proceed or error. //! TODO: That is not the case now, since call/create externalities traps on any error now. +//! +//! # Interaction with the system +//! +//! ## Finalization +//! +//! This module requires performing some finalization steps at the end of the block. If not performed +//! the module will have incorrect behavior. +//! +//! Call [`Module::execute`] at the end of the block. The order in relation to +//! the other module doesn't matter. +//! +//! ## Account killing +//! +//! When `staking` module determines that account is dead (e.g. account's balance fell below +//! exsistential deposit) then it reaps the account. That will lead to deletion of the associated +//! code and storage of the account. +//! +//! [`Module::execute`]: struct.Module.html#impl-Executable #![cfg_attr(not(feature = "std"), no_std)] @@ -90,16 +108,16 @@ use account_db::{AccountDb, OverlayAccountDb}; use double_map::StorageDoubleMap; use codec::Codec; -use runtime_primitives::traits::{As, RefInto, SimpleArithmetic}; +use runtime_primitives::traits::{As, RefInto, SimpleArithmetic, Executable}; use runtime_support::dispatch::Result; -use runtime_support::{Parameter, StorageMap}; +use runtime_support::{Parameter, StorageMap, StorageValue}; pub trait Trait: system::Trait + staking::Trait + consensus::Trait { /// Function type to get the contract address given the creator. type DetermineContractAddress: ContractAddressFor; // As is needed for wasm-utils - type Gas: Parameter + Codec + SimpleArithmetic + Copy + As + As + As; + type Gas: Parameter + Default + Codec + SimpleArithmetic + Copy + As + As + As; } pub trait ContractAddressFor { @@ -144,9 +162,13 @@ decl_storage! { GasPrice get(gas_price): b"con:gas_price" => required T::Balance; // The maximum nesting level of a call/create stack. MaxDepth get(max_depth): b"con:max_depth" => required u32; + // The maximum amount of gas that could be expended per block. + BlockGasLimit get(block_gas_limit): b"con:block_gas_limit" => required T::Gas; + // Gas spent so far in this block. + GasSpent get(gas_spent): b"con:gas_spent" => default T::Gas; // The code associated with an account. - pub CodeOf: b"con:cod:" => default map [ T::AccountId => Vec ]; // TODO Vec values should be optimised to not do a length prefix. + CodeOf: b"con:cod:" => default map [ T::AccountId => Vec ]; // TODO Vec values should be optimised to not do a length prefix. } // TODO: consider storing upper-bound for contract's gas limit in fixed-length runtime @@ -253,3 +275,10 @@ impl staking::OnFreeBalanceZero for Module { >::remove_prefix(who.clone()); } } + +/// Finalization hook for the smart-contract module. +impl Executable for Module { + fn execute() { + >::kill(); + } +} diff --git a/substrate/runtime/contract/src/tests.rs b/substrate/runtime/contract/src/tests.rs index 1f3d221ffe2c2..5f1014243a046 100644 --- a/substrate/runtime/contract/src/tests.rs +++ b/substrate/runtime/contract/src/tests.rs @@ -77,61 +77,90 @@ impl ContractAddressFor for DummyContractAddressFor { } } -fn new_test_ext(existential_deposit: u64, gas_price: u64) -> runtime_io::TestExternalities { - let mut t = system::GenesisConfig::::default() - .build_storage() - .unwrap(); - t.extend( - consensus::GenesisConfig:: { - code: vec![], - authorities: vec![], - }.build_storage() - .unwrap(), - ); - t.extend( - session::GenesisConfig:: { - session_length: 1, - validators: vec![10, 20], - }.build_storage() +struct ExtBuilder { + existential_deposit: u64, + gas_price: u64, + block_gas_limit: u64, +} +impl Default for ExtBuilder { + fn default() -> Self { + Self { + existential_deposit: 0, + gas_price: 2, + block_gas_limit: 100_000_000, + } + } +} +impl ExtBuilder { + fn existential_deposit(mut self, existential_deposit: u64) -> Self { + self.existential_deposit = existential_deposit; + self + } + fn gas_price(mut self, gas_price: u64) -> Self { + self.gas_price = gas_price; + self + } + fn block_gas_limit(mut self, block_gas_limit: u64) -> Self { + self.block_gas_limit = block_gas_limit; + self + } + fn build(self) -> runtime_io::TestExternalities { + let mut t = system::GenesisConfig::::default() + .build_storage() + .unwrap(); + t.extend( + consensus::GenesisConfig:: { + code: vec![], + authorities: vec![], + }.build_storage() .unwrap(), - ); - t.extend( - staking::GenesisConfig:: { - sessions_per_era: 1, - current_era: 0, - balances: vec![], - intentions: vec![], - validator_count: 2, - minimum_validator_count: 0, - bonding_duration: 0, - transaction_base_fee: 0, - transaction_byte_fee: 0, - existential_deposit: existential_deposit, - transfer_fee: 0, - creation_fee: 0, - reclaim_rebate: 0, - early_era_slash: 0, - session_reward: 0, - offline_slash_grace: 0, - }.build_storage() + ); + t.extend( + session::GenesisConfig:: { + session_length: 1, + validators: vec![10, 20], + }.build_storage() .unwrap(), - ); - t.extend( - timestamp::GenesisConfig::::default() - .build_storage() + ); + t.extend( + staking::GenesisConfig:: { + sessions_per_era: 1, + current_era: 0, + balances: vec![], + intentions: vec![], + validator_count: 2, + minimum_validator_count: 0, + bonding_duration: 0, + transaction_base_fee: 0, + transaction_byte_fee: 0, + existential_deposit: self.existential_deposit, + transfer_fee: 0, + creation_fee: 0, + reclaim_rebate: 0, + early_era_slash: 0, + session_reward: 0, + offline_slash_grace: 0, + }.build_storage() .unwrap(), - ); - t.extend( - GenesisConfig:: { - contract_fee: 21, - call_base_fee: 135, - create_base_fee: 175, - gas_price, - max_depth: 100, - }.build_storage() + ); + t.extend( + timestamp::GenesisConfig::::default() + .build_storage() + .unwrap(), + ); + t.extend( + GenesisConfig:: { + contract_fee: 21, + call_base_fee: 135, + create_base_fee: 175, + gas_price: self.gas_price, + max_depth: 100, + block_gas_limit: self.block_gas_limit, + }.build_storage() .unwrap(), - ); - t.into() + ); + t.into() + } } const CODE_TRANSFER: &str = r#" @@ -163,7 +192,7 @@ fn contract_transfer() { let code_transfer = wabt::wat2wasm(CODE_TRANSFER).unwrap(); - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { >::insert(1, code_transfer.to_vec()); Staking::set_free_balance(&0, 100_000_000); @@ -198,7 +227,7 @@ fn contract_transfer_oog() { let code_transfer = wabt::wat2wasm(CODE_TRANSFER).unwrap(); - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { >::insert(1, code_transfer.to_vec()); Staking::set_free_balance(&0, 100_000_000); @@ -219,16 +248,9 @@ fn contract_transfer_oog() { // 2 * 135 - base gas fee for call (by contract) 100_000_000 - (2 * 6) - (2 * 135) - (2 * 135), ); - assert_eq!( - Staking::free_balance(&1), - 11, - ); - assert_eq!( - Staking::free_balance(&CONTRACT_SHOULD_TRANSFER_TO), - 0, - ); + assert_eq!(Staking::free_balance(&1), 11); + assert_eq!(Staking::free_balance(&CONTRACT_SHOULD_TRANSFER_TO), 0); }); - } #[test] @@ -237,7 +259,7 @@ fn contract_transfer_max_depth() { let code_transfer = wabt::wat2wasm(CODE_TRANSFER).unwrap(); - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { >::insert(CONTRACT_SHOULD_TRANSFER_TO, code_transfer.to_vec()); Staking::set_free_balance(&0, 100_000_000); @@ -258,10 +280,7 @@ fn contract_transfer_max_depth() { // 2 * 135 * 100 - base gas fee for call (by transaction) multiplied by max depth (100). 100_000_000 - (2 * 135 * 100) - (2 * 6 * 100), ); - assert_eq!( - Staking::free_balance(&CONTRACT_SHOULD_TRANSFER_TO), - 11, - ); + assert_eq!(Staking::free_balance(&CONTRACT_SHOULD_TRANSFER_TO), 11); }); } @@ -340,7 +359,7 @@ fn contract_create() { let code_ctor_transfer = wabt::wat2wasm(&code_ctor(&code_transfer)).unwrap(); let code_create = wabt::wat2wasm(&code_create(&code_ctor_transfer)).unwrap(); - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { Staking::set_free_balance(&0, 100_000_000); Staking::increase_total_stake_by(100_000_000); Staking::set_free_balance(&1, 0); @@ -389,7 +408,7 @@ fn top_level_create() { let code_transfer = wabt::wat2wasm(CODE_TRANSFER).unwrap(); let code_ctor_transfer = wabt::wat2wasm(&code_ctor(&code_transfer)).unwrap(); - with_externalities(&mut new_test_ext(0, 3), || { + with_externalities(&mut ExtBuilder::default().gas_price(3).build(), || { let derived_address = ::DetermineContractAddress::contract_address_for( &code_ctor_transfer, &0, @@ -434,29 +453,29 @@ const CODE_NOP: &'static str = r#" fn refunds_unused_gas() { let code_nop = wabt::wat2wasm(CODE_NOP).unwrap(); - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { >::insert(1, code_nop.to_vec()); Staking::set_free_balance(&0, 100_000_000); Staking::increase_total_stake_by(100_000_000); - assert_ok!(Contract::call(&0, 1, 0, 100_000, Vec::new(),)); + assert_ok!(Contract::call(&0, 1, 0, 100_000, Vec::new())); - assert_eq!(Staking::free_balance(&0), 100_000_000 - 4 - (2 * 135),); + assert_eq!(Staking::free_balance(&0), 100_000_000 - 4 - (2 * 135)); }); } #[test] fn call_with_zero_value() { - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { >::insert(1, vec![]); Staking::set_free_balance(&0, 100_000_000); Staking::increase_total_stake_by(100_000_000); - assert_ok!(Contract::call(&0, 1, 0, 100_000, Vec::new(),)); + assert_ok!(Contract::call(&0, 1, 0, 100_000, Vec::new())); - assert_eq!(Staking::free_balance(&0), 100_000_000 - (2 * 135),); + assert_eq!(Staking::free_balance(&0), 100_000_000 - (2 * 135)); }); } @@ -464,11 +483,11 @@ fn call_with_zero_value() { fn create_with_zero_endowment() { let code_nop = wabt::wat2wasm(CODE_NOP).unwrap(); - with_externalities(&mut new_test_ext(0, 2), || { + with_externalities(&mut ExtBuilder::default().build(), || { Staking::set_free_balance(&0, 100_000_000); Staking::increase_total_stake_by(100_000_000); - assert_ok!(Contract::create(&0, 0, 100_000, code_nop, Vec::new(),)); + assert_ok!(Contract::create(&0, 0, 100_000, code_nop, Vec::new())); assert_eq!( Staking::free_balance(&0), @@ -481,42 +500,45 @@ fn create_with_zero_endowment() { #[test] fn account_removal_removes_storage() { - with_externalities(&mut new_test_ext(100, 2), || { - // Setup two accounts with free balance above than exsistential threshold. - { - Staking::set_free_balance(&1, 110); - Staking::increase_total_stake_by(110); - >::insert(1, b"foo".to_vec(), b"1".to_vec()); - >::insert(1, b"bar".to_vec(), b"2".to_vec()); - - Staking::set_free_balance(&2, 110); - Staking::increase_total_stake_by(110); - >::insert(2, b"hello".to_vec(), b"3".to_vec()); - >::insert(2, b"world".to_vec(), b"4".to_vec()); - } - - // Transfer funds from account 1 of such amount that after this transfer - // the balance of account 1 is will be below than exsistential threshold. - // - // This should lead to the removal of all storage associated with this account. - assert_ok!(Staking::transfer(&1, 2.into(), 20)); - - // Verify that all entries from account 1 is removed, while - // entries from account 2 is in place. - { - assert_eq!(>::get(1, b"foo".to_vec()), None); - assert_eq!(>::get(1, b"bar".to_vec()), None); - - assert_eq!( - >::get(2, b"hello".to_vec()), - Some(b"3".to_vec()) - ); - assert_eq!( - >::get(2, b"world".to_vec()), - Some(b"4".to_vec()) - ); - } - }); + with_externalities( + &mut ExtBuilder::default().existential_deposit(100).build(), + || { + // Setup two accounts with free balance above than exsistential threshold. + { + Staking::set_free_balance(&1, 110); + Staking::increase_total_stake_by(110); + >::insert(1, b"foo".to_vec(), b"1".to_vec()); + >::insert(1, b"bar".to_vec(), b"2".to_vec()); + + Staking::set_free_balance(&2, 110); + Staking::increase_total_stake_by(110); + >::insert(2, b"hello".to_vec(), b"3".to_vec()); + >::insert(2, b"world".to_vec(), b"4".to_vec()); + } + + // Transfer funds from account 1 of such amount that after this transfer + // the balance of account 1 is will be below than exsistential threshold. + // + // This should lead to the removal of all storage associated with this account. + assert_ok!(Staking::transfer(&1, 2.into(), 20)); + + // Verify that all entries from account 1 is removed, while + // entries from account 2 is in place. + { + assert_eq!(>::get(1, b"foo".to_vec()), None); + assert_eq!(>::get(1, b"bar".to_vec()), None); + + assert_eq!( + >::get(2, b"hello".to_vec()), + Some(b"3".to_vec()) + ); + assert_eq!( + >::get(2, b"world".to_vec()), + Some(b"4".to_vec()) + ); + } + }, + ); } const CODE_UNREACHABLE: &'static str = r#" @@ -531,7 +553,7 @@ const CODE_UNREACHABLE: &'static str = r#" #[test] fn top_level_call_refunds_even_if_fails() { let code_unreachable = wabt::wat2wasm(CODE_UNREACHABLE).unwrap(); - with_externalities(&mut new_test_ext(0, 4), || { + with_externalities(&mut ExtBuilder::default().gas_price(4).build(), || { >::insert(1, code_unreachable.to_vec()); Staking::set_free_balance(&0, 100_000_000); @@ -542,6 +564,48 @@ fn top_level_call_refunds_even_if_fails() { "vm execute returned error while call" ); - assert_eq!(Staking::free_balance(&0), 100_000_000 - (4 * 3) - (4 * 135),); + assert_eq!(Staking::free_balance(&0), 100_000_000 - (4 * 3) - (4 * 135)); }); } + +const CODE_LOOP: &'static str = r#" +(module + (func (export "call") + (loop + (br 0) + ) + ) +) +"#; + +#[test] +fn block_gas_limit() { + let code_loop = wabt::wat2wasm(CODE_LOOP).unwrap(); + with_externalities( + &mut ExtBuilder::default().block_gas_limit(100_000).build(), + || { + >::insert(1, code_loop.to_vec()); + + Staking::set_free_balance(&0, 100_000_000); + Staking::increase_total_stake_by(100_000_000); + + // Spend 50_000 units of gas (OOG). + assert_err!( + Contract::call(&0, 1, 0, 50_000, Vec::new()), + "vm execute returned error while call" + ); + + // Ensure we can't spend more gas than available in block gas limit. + assert_err!( + Contract::call(&0, 1, 0, 50_001, Vec::new()), + "block gas limit is reached" + ); + + // However, we can spend another 50_000 + assert_err!( + Contract::call(&0, 1, 0, 50_000, Vec::new()), + "vm execute returned error while call" + ); + }, + ); +}