Contracts: Per block gas limit (#506)

* Add block gas limit check

* Fix formatting

* Add block_gas_limit test.

* Use ExtBuilder in tests

* Docs and clean-up.

* Correct style
This commit is contained in:
Sergey Pepyakin
2018-08-28 18:58:27 +03:00
committed by Gav Wood
parent 3e63009ac7
commit 20655af97b
4 changed files with 243 additions and 122 deletions
@@ -14,8 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
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<T: Trait> {
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<T: Trait> GasMeter<T> {
#[cfg(test)]
pub fn with_limit(gas_limit: T::Gas, gas_price: T::Balance) -> GasMeter<T> {
GasMeter {
limit: gas_limit,
gas_left: gas_limit,
gas_price,
}
@@ -101,6 +105,7 @@ impl<T: Trait> GasMeter<T> {
} 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<T: Trait> GasMeter<T> {
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<T: Trait>(
transactor: &T::AccountId,
gas_limit: T::Gas,
) -> Result<GasMeter<T>, &'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 = <Module<T>>::block_gas_limit() - <Module<T>>::gas_spent();
if gas_limit > gas_available {
return Err("block gas limit is reached");
}
// Buy the specified amount of gas.
let gas_price = <Module<T>>::gas_price();
let b = <staking::Module<T>>::free_balance(transactor);
let cost = <T::Gas as As<T::Balance>>::as_(gas_limit.clone())
@@ -138,6 +156,7 @@ pub fn buy_gas<T: Trait>(
<staking::Module<T>>::set_free_balance(transactor, b - cost);
<staking::Module<T>>::decrease_total_stake_by(cost);
Ok(GasMeter {
limit: gas_limit,
gas_left: gas_limit,
gas_price,
})
@@ -145,6 +164,13 @@ pub fn buy_gas<T: Trait>(
/// Refund the unused gas.
pub fn refund_unused_gas<T: Trait>(transactor: &T::AccountId, gas_meter: GasMeter<T>) {
// 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 = <Module<T>>::gas_spent() + gas_meter.spent();
<GasSpent<T>>::put(gas_spent);
// Refund gas left by the price it was bought.
let b = <staking::Module<T>>::free_balance(transactor);
let refund = <T::Gas as As<T::Balance>>::as_(gas_meter.gas_left) * gas_meter.gas_price;
<staking::Module<T>>::set_free_balance(transactor, b + refund);
@@ -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<T: Trait> {
pub create_base_fee: T::Gas,
pub gas_price: T::Balance,
pub max_depth: u32,
pub block_gas_limit: T::Gas,
}
impl<T: Trait> runtime_primitives::BuildStorage for GenesisConfig<T> {
@@ -43,7 +44,8 @@ impl<T: Trait> runtime_primitives::BuildStorage for GenesisConfig<T> {
twox_128(<CallBaseFee<T>>::key()).to_vec() => self.call_base_fee.encode(),
twox_128(<CreateBaseFee<T>>::key()).to_vec() => self.create_base_fee.encode(),
twox_128(<GasPrice<T>>::key()).to_vec() => self.gas_price.encode(),
twox_128(<MaxDepth<T>>::key()).to_vec() => self.max_depth.encode()
twox_128(<MaxDepth<T>>::key()).to_vec() => self.max_depth.encode(),
twox_128(<BlockGasLimit<T>>::key()).to_vec() => self.block_gas_limit.encode()
];
Ok(r.into())
}
@@ -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<Self::AccountId>;
// As<u32> is needed for wasm-utils
type Gas: Parameter + Codec + SimpleArithmetic + Copy + As<Self::Balance> + As<u64> + As<u32>;
type Gas: Parameter + Default + Codec + SimpleArithmetic + Copy + As<Self::Balance> + As<u64> + As<u32>;
}
pub trait ContractAddressFor<AccountId: Sized> {
@@ -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<u8> ]; // TODO Vec<u8> values should be optimised to not do a length prefix.
CodeOf: b"con:cod:" => default map [ T::AccountId => Vec<u8> ]; // TODO Vec<u8> 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<T: Trait> staking::OnFreeBalanceZero<T::AccountId> for Module<T> {
<StorageOf<T>>::remove_prefix(who.clone());
}
}
/// Finalization hook for the smart-contract module.
impl<T: Trait> Executable for Module<T> {
fn execute() {
<GasSpent<T>>::kill();
}
}
+176 -112
View File
@@ -77,61 +77,90 @@ impl ContractAddressFor<u64> for DummyContractAddressFor {
}
}
fn new_test_ext(existential_deposit: u64, gas_price: u64) -> runtime_io::TestExternalities<KeccakHasher> {
let mut t = system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
t.extend(
consensus::GenesisConfig::<Test> {
code: vec![],
authorities: vec![],
}.build_storage()
.unwrap(),
);
t.extend(
session::GenesisConfig::<Test> {
session_length: 1,
validators: vec![10, 20],
}.build_storage()
.unwrap(),
);
t.extend(
staking::GenesisConfig::<Test> {
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()
.unwrap(),
);
t.extend(
timestamp::GenesisConfig::<Test>::default()
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<KeccakHasher> {
let mut t = system::GenesisConfig::<Test>::default()
.build_storage()
.unwrap();
t.extend(
consensus::GenesisConfig::<Test> {
code: vec![],
authorities: vec![],
}.build_storage()
.unwrap(),
);
t.extend(
GenesisConfig::<Test> {
contract_fee: 21,
call_base_fee: 135,
create_base_fee: 175,
gas_price,
max_depth: 100,
}.build_storage()
);
t.extend(
session::GenesisConfig::<Test> {
session_length: 1,
validators: vec![10, 20],
}.build_storage()
.unwrap(),
);
t.into()
);
t.extend(
staking::GenesisConfig::<Test> {
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(
timestamp::GenesisConfig::<Test>::default()
.build_storage()
.unwrap(),
);
t.extend(
GenesisConfig::<Test> {
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()
}
}
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(), || {
<CodeOf<Test>>::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(), || {
<CodeOf<Test>>::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(), || {
<CodeOf<Test>>::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 = <Test as Trait>::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(), || {
<CodeOf<Test>>::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(), || {
<CodeOf<Test>>::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);
<StorageOf<Test>>::insert(1, b"foo".to_vec(), b"1".to_vec());
<StorageOf<Test>>::insert(1, b"bar".to_vec(), b"2".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);
<StorageOf<Test>>::insert(1, b"foo".to_vec(), b"1".to_vec());
<StorageOf<Test>>::insert(1, b"bar".to_vec(), b"2".to_vec());
Staking::set_free_balance(&2, 110);
Staking::increase_total_stake_by(110);
<StorageOf<Test>>::insert(2, b"hello".to_vec(), b"3".to_vec());
<StorageOf<Test>>::insert(2, b"world".to_vec(), b"4".to_vec());
}
Staking::set_free_balance(&2, 110);
Staking::increase_total_stake_by(110);
<StorageOf<Test>>::insert(2, b"hello".to_vec(), b"3".to_vec());
<StorageOf<Test>>::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));
// 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!(<StorageOf<Test>>::get(1, b"foo".to_vec()), None);
assert_eq!(<StorageOf<Test>>::get(1, b"bar".to_vec()), None);
// Verify that all entries from account 1 is removed, while
// entries from account 2 is in place.
{
assert_eq!(<StorageOf<Test>>::get(1, b"foo".to_vec()), None);
assert_eq!(<StorageOf<Test>>::get(1, b"bar".to_vec()), None);
assert_eq!(
<StorageOf<Test>>::get(2, b"hello".to_vec()),
Some(b"3".to_vec())
);
assert_eq!(
<StorageOf<Test>>::get(2, b"world".to_vec()),
Some(b"4".to_vec())
);
}
});
assert_eq!(
<StorageOf<Test>>::get(2, b"hello".to_vec()),
Some(b"3".to_vec())
);
assert_eq!(
<StorageOf<Test>>::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(), || {
<CodeOf<Test>>::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(),
|| {
<CodeOf<Test>>::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"
);
},
);
}