contracts: Lazy storage removal (#7740)

* Do not evict a contract from within a call stack

We don't want to trigger contract eviction automatically when
a contract is called. This is because those changes can be
reverted due to how storage transactions are used at the moment.
More Information:
https://github.com/paritytech/substrate/issues/6439#issuecomment-648754324

It can be re-introduced once the linked issue is resolved. In the meantime
`claim_surcharge` must be called to evict a contract.

* Lazily delete storage in on_initialize instead of when removing the contract

* Add missing documentation of new error

* Make Module::claim_surcharge public

It being the only dispatchable that is private is an oversight.

* review: Add final newline

* review: Simplify assert statement

* Add test that checks that partial remove of a contract works

* Premote warning to error

* Added missing docs for seal_terminate

* Lazy deletion should only take AVERAGE_ON_INITIALIZE_RATIO of the block

* Added informational about the lazy deletion throughput

* Avoid lazy deletion in case the block is already full

* Prevent queue decoding in case of an already full block

* Add test that checks that on_initialize honors block limits
This commit is contained in:
Alexander Theißen
2021-01-04 13:35:57 +01:00
committed by GitHub
parent f0b99dd2f2
commit 3ba8fdfc11
9 changed files with 1348 additions and 525 deletions
@@ -39,7 +39,7 @@ use self::{
use frame_benchmarking::{benchmarks, account, whitelisted_caller};
use frame_system::{Module as System, RawOrigin};
use parity_wasm::elements::{Instruction, ValueType, BlockType};
use sp_runtime::traits::{Hash, Bounded};
use sp_runtime::traits::{Hash, Bounded, Zero};
use sp_std::{default::Default, convert::{TryInto}, vec::Vec, vec};
use pallet_contracts_primitives::RentProjection;
@@ -209,37 +209,52 @@ where
}
}
/// A `Contract` that was evicted after accumulating some storage.
/// A `Contract` that contains some storage items.
///
/// This is used to benchmark contract resurrection.
struct Tombstone<T: Config> {
/// This is used to benchmark contract destruction and resurection. Those operations'
/// weight depend on the amount of storage accumulated.
struct ContractWithStorage<T: Config> {
/// The contract that was evicted.
contract: Contract<T>,
/// The storage the contract held when it was avicted.
storage: Vec<(StorageKey, Vec<u8>)>,
}
impl<T: Config> Tombstone<T>
impl<T: Config> ContractWithStorage<T>
where
T: Config,
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
/// Create and evict a new contract with the supplied storage item count and size each.
/// Same as [`Self::with_code`] but with dummy contract code.
fn new(stor_num: u32, stor_size: u32) -> Result<Self, &'static str> {
let contract = Contract::<T>::new(WasmModule::dummy(), vec![], Endow::CollectRent)?;
Self::with_code(WasmModule::dummy(), stor_num, stor_size)
}
/// Create and evict a new contract with the supplied storage item count and size each.
fn with_code(code: WasmModule<T>, stor_num: u32, stor_size: u32) -> Result<Self, &'static str> {
let contract = Contract::<T>::new(code, vec![], Endow::CollectRent)?;
let storage_items = create_storage::<T>(stor_num, stor_size)?;
contract.store(&storage_items)?;
System::<T>::set_block_number(
contract.eviction_at()? + T::SignedClaimHandicap::get() + 5u32.into()
);
Rent::<T>::collect(&contract.account_id);
contract.ensure_tombstone()?;
Ok(Tombstone {
Ok(Self {
contract,
storage: storage_items,
})
}
/// Increase the system block number so that this contract is eligible for eviction.
fn set_block_num_for_eviction(&self) -> Result<(), &'static str> {
System::<T>::set_block_number(
self.contract.eviction_at()? + T::SignedClaimHandicap::get() + 5u32.into()
);
Ok(())
}
/// Evict this contract.
fn evict(&mut self) -> Result<(), &'static str> {
self.set_block_num_for_eviction()?;
Rent::<T>::snitch_contract_should_be_evicted(&self.contract.account_id, Zero::zero())?;
self.contract.ensure_tombstone()
}
}
/// Generate `stor_num` storage items. Each has the size `stor_size`.
@@ -270,6 +285,30 @@ benchmarks! {
_ {
}
// The base weight without any actual work performed apart from the setup costs.
on_initialize {}: {
Storage::<T>::process_deletion_queue_batch(Weight::max_value())
}
on_initialize_per_trie_key {
let k in 0..1024;
let instance = ContractWithStorage::<T>::new(k, T::MaxValueSize::get())?;
Storage::<T>::queue_trie_for_deletion(&instance.contract.alive_info()?)?;
}: {
Storage::<T>::process_deletion_queue_batch(Weight::max_value())
}
on_initialize_per_queue_item {
let q in 0..1024.min(T::DeletionQueueDepth::get());
for i in 0 .. q {
let instance = Contract::<T>::with_index(i, WasmModule::dummy(), vec![], Endow::Max)?;
Storage::<T>::queue_trie_for_deletion(&instance.alive_info()?)?;
ContractInfoOf::<T>::remove(instance.account_id);
}
}: {
Storage::<T>::process_deletion_queue_batch(Weight::max_value())
}
// This extrinsic is pretty much constant as it is only a simple setter.
update_schedule {
let schedule = Schedule {
@@ -650,7 +689,8 @@ benchmarks! {
// Restore just moves the trie id from origin to destination and therefore
// does not depend on the size of the destination contract. However, to not
// trigger any edge case we won't use an empty contract as destination.
let tombstone = Tombstone::<T>::new(10, T::MaxValueSize::get())?;
let mut tombstone = ContractWithStorage::<T>::new(10, T::MaxValueSize::get())?;
tombstone.evict()?;
let dest = tombstone.contract.account_id.encode();
let dest_len = dest.len();
@@ -723,7 +763,8 @@ benchmarks! {
seal_restore_to_per_delta {
let d in 0 .. API_BENCHMARK_BATCHES;
let tombstone = Tombstone::<T>::new(0, 0)?;
let mut tombstone = ContractWithStorage::<T>::new(0, 0)?;
tombstone.evict()?;
let delta = create_storage::<T>(d * API_BENCHMARK_BATCH_SIZE, T::MaxValueSize::get())?;
let dest = tombstone.contract.account_id.encode();
@@ -2368,7 +2409,20 @@ benchmarks! {
#[extra]
print_schedule {
#[cfg(feature = "std")]
println!("{:#?}", Schedule::<T>::default());
{
let weight_per_key = T::WeightInfo::on_initialize_per_trie_key(1) -
T::WeightInfo::on_initialize_per_trie_key(0);
let weight_per_queue_item = T::WeightInfo::on_initialize_per_queue_item(1) -
T::WeightInfo::on_initialize_per_queue_item(0);
let weight_limit = T::DeletionWeightLimit::get();
let queue_depth: u64 = T::DeletionQueueDepth::get().into();
println!("{:#?}", Schedule::<T>::default());
println!("###############################################");
println!("Lazy deletion throughput per block (empty queue, full queue): {}, {}",
weight_limit / weight_per_key,
(weight_limit - weight_per_queue_item * queue_depth) / weight_per_key,
);
}
#[cfg(not(feature = "std"))]
return Err("Run this bench with a native runtime in order to see the schedule.");
}: {}
@@ -2394,6 +2448,10 @@ mod tests {
}
}
create_test!(on_initialize);
create_test!(on_initialize_per_trie_key);
create_test!(on_initialize_per_queue_item);
create_test!(update_schedule);
create_test!(put_code);
create_test!(instantiate);