Skip to content

Commit e97f753

Browse files
authored
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: paritytech/substrate#6439 (comment) 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
1 parent 001173b commit e97f753

File tree

8 files changed

+1336
-525
lines changed

8 files changed

+1336
-525
lines changed

src/benchmarking/mod.rs

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use self::{
3939
use frame_benchmarking::{benchmarks, account, whitelisted_caller};
4040
use frame_system::{Module as System, RawOrigin};
4141
use parity_wasm::elements::{Instruction, ValueType, BlockType};
42-
use sp_runtime::traits::{Hash, Bounded};
42+
use sp_runtime::traits::{Hash, Bounded, Zero};
4343
use sp_std::{default::Default, convert::{TryInto}, vec::Vec, vec};
4444
use pallet_contracts_primitives::RentProjection;
4545

@@ -209,37 +209,52 @@ where
209209
}
210210
}
211211

212-
/// A `Contract` that was evicted after accumulating some storage.
212+
/// A `Contract` that contains some storage items.
213213
///
214-
/// This is used to benchmark contract resurrection.
215-
struct Tombstone<T: Config> {
214+
/// This is used to benchmark contract destruction and resurection. Those operations'
215+
/// weight depend on the amount of storage accumulated.
216+
struct ContractWithStorage<T: Config> {
216217
/// The contract that was evicted.
217218
contract: Contract<T>,
218219
/// The storage the contract held when it was avicted.
219220
storage: Vec<(StorageKey, Vec<u8>)>,
220221
}
221222

222-
impl<T: Config> Tombstone<T>
223+
impl<T: Config> ContractWithStorage<T>
223224
where
224225
T: Config,
225226
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
226227
{
227-
/// Create and evict a new contract with the supplied storage item count and size each.
228+
/// Same as [`Self::with_code`] but with dummy contract code.
228229
fn new(stor_num: u32, stor_size: u32) -> Result<Self, &'static str> {
229-
let contract = Contract::<T>::new(WasmModule::dummy(), vec![], Endow::CollectRent)?;
230+
Self::with_code(WasmModule::dummy(), stor_num, stor_size)
231+
}
232+
233+
/// Create and evict a new contract with the supplied storage item count and size each.
234+
fn with_code(code: WasmModule<T>, stor_num: u32, stor_size: u32) -> Result<Self, &'static str> {
235+
let contract = Contract::<T>::new(code, vec![], Endow::CollectRent)?;
230236
let storage_items = create_storage::<T>(stor_num, stor_size)?;
231237
contract.store(&storage_items)?;
232-
System::<T>::set_block_number(
233-
contract.eviction_at()? + T::SignedClaimHandicap::get() + 5u32.into()
234-
);
235-
Rent::<T>::collect(&contract.account_id);
236-
contract.ensure_tombstone()?;
237-
238-
Ok(Tombstone {
238+
Ok(Self {
239239
contract,
240240
storage: storage_items,
241241
})
242242
}
243+
244+
/// Increase the system block number so that this contract is eligible for eviction.
245+
fn set_block_num_for_eviction(&self) -> Result<(), &'static str> {
246+
System::<T>::set_block_number(
247+
self.contract.eviction_at()? + T::SignedClaimHandicap::get() + 5u32.into()
248+
);
249+
Ok(())
250+
}
251+
252+
/// Evict this contract.
253+
fn evict(&mut self) -> Result<(), &'static str> {
254+
self.set_block_num_for_eviction()?;
255+
Rent::<T>::snitch_contract_should_be_evicted(&self.contract.account_id, Zero::zero())?;
256+
self.contract.ensure_tombstone()
257+
}
243258
}
244259

245260
/// Generate `stor_num` storage items. Each has the size `stor_size`.
@@ -270,6 +285,30 @@ benchmarks! {
270285
_ {
271286
}
272287

288+
// The base weight without any actual work performed apart from the setup costs.
289+
on_initialize {}: {
290+
Storage::<T>::process_deletion_queue_batch(Weight::max_value())
291+
}
292+
293+
on_initialize_per_trie_key {
294+
let k in 0..1024;
295+
let instance = ContractWithStorage::<T>::new(k, T::MaxValueSize::get())?;
296+
Storage::<T>::queue_trie_for_deletion(&instance.contract.alive_info()?)?;
297+
}: {
298+
Storage::<T>::process_deletion_queue_batch(Weight::max_value())
299+
}
300+
301+
on_initialize_per_queue_item {
302+
let q in 0..1024.min(T::DeletionQueueDepth::get());
303+
for i in 0 .. q {
304+
let instance = Contract::<T>::with_index(i, WasmModule::dummy(), vec![], Endow::Max)?;
305+
Storage::<T>::queue_trie_for_deletion(&instance.alive_info()?)?;
306+
ContractInfoOf::<T>::remove(instance.account_id);
307+
}
308+
}: {
309+
Storage::<T>::process_deletion_queue_batch(Weight::max_value())
310+
}
311+
273312
// This extrinsic is pretty much constant as it is only a simple setter.
274313
update_schedule {
275314
let schedule = Schedule {
@@ -650,7 +689,8 @@ benchmarks! {
650689
// Restore just moves the trie id from origin to destination and therefore
651690
// does not depend on the size of the destination contract. However, to not
652691
// trigger any edge case we won't use an empty contract as destination.
653-
let tombstone = Tombstone::<T>::new(10, T::MaxValueSize::get())?;
692+
let mut tombstone = ContractWithStorage::<T>::new(10, T::MaxValueSize::get())?;
693+
tombstone.evict()?;
654694

655695
let dest = tombstone.contract.account_id.encode();
656696
let dest_len = dest.len();
@@ -723,7 +763,8 @@ benchmarks! {
723763

724764
seal_restore_to_per_delta {
725765
let d in 0 .. API_BENCHMARK_BATCHES;
726-
let tombstone = Tombstone::<T>::new(0, 0)?;
766+
let mut tombstone = ContractWithStorage::<T>::new(0, 0)?;
767+
tombstone.evict()?;
727768
let delta = create_storage::<T>(d * API_BENCHMARK_BATCH_SIZE, T::MaxValueSize::get())?;
728769

729770
let dest = tombstone.contract.account_id.encode();
@@ -2368,7 +2409,20 @@ benchmarks! {
23682409
#[extra]
23692410
print_schedule {
23702411
#[cfg(feature = "std")]
2371-
println!("{:#?}", Schedule::<T>::default());
2412+
{
2413+
let weight_per_key = T::WeightInfo::on_initialize_per_trie_key(1) -
2414+
T::WeightInfo::on_initialize_per_trie_key(0);
2415+
let weight_per_queue_item = T::WeightInfo::on_initialize_per_queue_item(1) -
2416+
T::WeightInfo::on_initialize_per_queue_item(0);
2417+
let weight_limit = T::DeletionWeightLimit::get();
2418+
let queue_depth: u64 = T::DeletionQueueDepth::get().into();
2419+
println!("{:#?}", Schedule::<T>::default());
2420+
println!("###############################################");
2421+
println!("Lazy deletion throughput per block (empty queue, full queue): {}, {}",
2422+
weight_limit / weight_per_key,
2423+
(weight_limit - weight_per_queue_item * queue_depth) / weight_per_key,
2424+
);
2425+
}
23722426
#[cfg(not(feature = "std"))]
23732427
return Err("Run this bench with a native runtime in order to see the schedule.");
23742428
}: {}
@@ -2394,6 +2448,10 @@ mod tests {
23942448
}
23952449
}
23962450

2451+
create_test!(on_initialize);
2452+
create_test!(on_initialize_per_trie_key);
2453+
create_test!(on_initialize_per_queue_item);
2454+
23972455
create_test!(update_schedule);
23982456
create_test!(put_code);
23992457
create_test!(instantiate);

src/exec.rs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -268,12 +268,12 @@ where
268268
Err(Error::<T>::MaxCallDepthReached)?
269269
}
270270

271-
// Assumption: `collect` doesn't collide with overlay because
272-
// `collect` will be done on first call and destination contract and balance
273-
// cannot be changed before the first call
274-
// We do not allow 'calling' plain accounts. For transfering value
275-
// `seal_transfer` must be used.
276-
let contract = if let Some(ContractInfo::Alive(info)) = Rent::<T>::collect(&dest) {
271+
// This charges the rent and denies access to a contract that is in need of
272+
// eviction by returning `None`. We cannot evict eagerly here because those
273+
// changes would be rolled back in case this contract is called by another
274+
// contract.
275+
// See: https://github.com/paritytech/substrate/issues/6439#issuecomment-648754324
276+
let contract = if let Ok(Some(ContractInfo::Alive(info))) = Rent::<T>::charge(&dest) {
277277
info
278278
} else {
279279
Err(Error::<T>::NotCallable)?
@@ -575,13 +575,16 @@ where
575575
value,
576576
self.ctx,
577577
)?;
578-
let self_trie_id = self.ctx.self_trie_id.as_ref().expect(
579-
"this function is only invoked by in the context of a contract;\
580-
a contract has a trie id;\
581-
this can't be None; qed",
582-
);
583-
Storage::<T>::destroy_contract(&self_id, self_trie_id);
584-
Ok(())
578+
if let Some(ContractInfo::Alive(info)) = ContractInfoOf::<T>::take(&self_id) {
579+
Storage::<T>::queue_trie_for_deletion(&info)?;
580+
Ok(())
581+
} else {
582+
panic!(
583+
"this function is only invoked by in the context of a contract;\
584+
this contract is therefore alive;\
585+
qed"
586+
);
587+
}
585588
}
586589

587590
fn call(

src/lib.rs

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ use frame_support::{
123123
dispatch::{DispatchResult, DispatchResultWithPostInfo},
124124
traits::{OnUnbalanced, Currency, Get, Time, Randomness},
125125
};
126-
use frame_system::{ensure_signed, ensure_root};
126+
use frame_system::{ensure_signed, ensure_root, Module as System};
127127
use pallet_contracts_primitives::{
128128
RentProjectionResult, GetStorageResult, ContractAccessError, ContractExecResult, ExecResult,
129129
};
@@ -325,6 +325,12 @@ pub trait Config: frame_system::Config {
325325

326326
/// Type that allows the runtime authors to add new host functions for a contract to call.
327327
type ChainExtension: chain_extension::ChainExtension;
328+
329+
/// The maximum number of tries that can be queued for deletion.
330+
type DeletionQueueDepth: Get<u32>;
331+
332+
/// The maximum amount of weight that can be consumed per block for lazy trie removal.
333+
type DeletionWeightLimit: Get<Weight>;
328334
}
329335

330336
decl_error! {
@@ -396,6 +402,17 @@ decl_error! {
396402
/// in this error. Note that this usually shouldn't happen as deploying such contracts
397403
/// is rejected.
398404
NoChainExtension,
405+
/// Removal of a contract failed because the deletion queue is full.
406+
///
407+
/// This can happen when either calling [`Module::claim_surcharge`] or `seal_terminate`.
408+
/// The queue is filled by deleting contracts and emptied by a fixed amount each block.
409+
/// Trying again during another block is the only way to resolve this issue.
410+
DeletionQueueFull,
411+
/// A contract could not be evicted because it has enough balance to pay rent.
412+
///
413+
/// This can be returned from [`Module::claim_surcharge`] because the target
414+
/// contract has enough balance to pay for its rent.
415+
ContractNotEvictable,
399416
}
400417
}
401418

@@ -449,8 +466,24 @@ decl_module! {
449466
/// The maximum size of a storage value in bytes. A reasonable default is 16 KiB.
450467
const MaxValueSize: u32 = T::MaxValueSize::get();
451468

469+
/// The maximum number of tries that can be queued for deletion.
470+
const DeletionQueueDepth: u32 = T::DeletionQueueDepth::get();
471+
472+
/// The maximum amount of weight that can be consumed per block for lazy trie removal.
473+
const DeletionWeightLimit: Weight = T::DeletionWeightLimit::get();
474+
452475
fn deposit_event() = default;
453476

477+
fn on_initialize() -> Weight {
478+
// We do not want to go above the block limit and rather avoid lazy deletion
479+
// in that case. This should only happen on runtime upgrades.
480+
let weight_limit = T::BlockWeights::get().max_block
481+
.saturating_sub(System::<T>::block_weight().total())
482+
.min(T::DeletionWeightLimit::get());
483+
Storage::<T>::process_deletion_queue_batch(weight_limit)
484+
.saturating_add(T::WeightInfo::on_initialize())
485+
}
486+
454487
/// Updates the schedule for metering contracts.
455488
///
456489
/// The schedule must have a greater version than the stored schedule.
@@ -549,10 +582,14 @@ decl_module! {
549582
/// Allows block producers to claim a small reward for evicting a contract. If a block producer
550583
/// fails to do so, a regular users will be allowed to claim the reward.
551584
///
552-
/// If contract is not evicted as a result of this call, no actions are taken and
553-
/// the sender is not eligible for the reward.
585+
/// If contract is not evicted as a result of this call, [`Error::ContractNotEvictable`]
586+
/// is returned and the sender is not eligible for the reward.
554587
#[weight = T::WeightInfo::claim_surcharge()]
555-
fn claim_surcharge(origin, dest: T::AccountId, aux_sender: Option<T::AccountId>) {
588+
pub fn claim_surcharge(
589+
origin,
590+
dest: T::AccountId,
591+
aux_sender: Option<T::AccountId>
592+
) -> DispatchResult {
556593
let origin = origin.into();
557594
let (signed, rewarded) = match (origin, aux_sender) {
558595
(Ok(frame_system::RawOrigin::Signed(account)), None) => {
@@ -574,8 +611,10 @@ decl_module! {
574611
};
575612

576613
// If poking the contract has lead to eviction of the contract, give out the rewards.
577-
if Rent::<T>::snitch_contract_should_be_evicted(&dest, handicap) {
578-
T::Currency::deposit_into_existing(&rewarded, T::SurchargeReward::get())?;
614+
if Rent::<T>::snitch_contract_should_be_evicted(&dest, handicap)? {
615+
T::Currency::deposit_into_existing(&rewarded, T::SurchargeReward::get()).map(|_| ())
616+
} else {
617+
Err(Error::<T>::ContractNotEvictable.into())
579618
}
580619
}
581620
}
@@ -733,6 +772,11 @@ decl_storage! {
733772
///
734773
/// TWOX-NOTE: SAFE since `AccountId` is a secure hash.
735774
pub ContractInfoOf: map hasher(twox_64_concat) T::AccountId => Option<ContractInfo<T>>;
775+
/// Evicted contracts that await child trie deletion.
776+
///
777+
/// Child trie deletion is a heavy operation depending on the amount of storage items
778+
/// stored in said trie. Therefore this operation is performed lazily in `on_initialize`.
779+
pub DeletionQueue: Vec<storage::DeletedContract>;
736780
}
737781
}
738782

0 commit comments

Comments
 (0)