Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Fix lazy batch contract removal #10728

6 changes: 4 additions & 2 deletions frame/contracts/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,15 +268,17 @@ where

let mut queue = <DeletionQueue<T>>::get();

if let (Some(trie), true) = (queue.get(0), remaining_key_budget > 0) {
while !queue.is_empty() && remaining_key_budget > 0 {
// Cannot panic due to loop condition
let trie = &mut queue[0];
let outcome =
child::kill_storage(&child_trie_info(&trie.trie_id), Some(remaining_key_budget));
let keys_removed = match outcome {
// This happens when our budget wasn't large enough to remove all keys.
KillStorageResult::SomeRemaining(count) => count,
KillStorageResult::AllRemoved(count) => {
// We do not care to preserve order. The contract is deleted already and
// noone waits for the trie to be deleted.
// no one waits for the trie to be deleted.
queue.swap_remove(0);
count
},
Expand Down
53 changes: 53 additions & 0 deletions frame/contracts/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1513,6 +1513,59 @@ fn lazy_removal_works() {
});
}

#[test]
fn lazy_batch_removal_works() {
let (code, hash) = compile_module::<Test>("self_destruct").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let min_balance = <Test as Config>::Currency::minimum_balance();
let _ = Balances::deposit_creating(&ALICE, 1000 * min_balance);
let mut tries: Vec<child::ChildInfo> = vec![];

for i in 0..3u8 {
assert_ok!(Contracts::instantiate_with_code(
Origin::signed(ALICE),
min_balance * 100,
GAS_LIMIT,
None,
code.clone(),
vec![],
vec![i],
),);

let addr = Contracts::contract_address(&ALICE, &hash, &[i]);
let info = <ContractInfoOf<Test>>::get(&addr).unwrap();
let trie = &info.child_trie_info();

// Put value into the contracts child trie
child::put(trie, &[99], &42);

// Terminate the contract. Contract info should be gone, but value should be still there
// as the lazy removal did not run, yet.
assert_ok!(Contracts::call(
Origin::signed(ALICE),
addr.clone(),
0,
GAS_LIMIT,
None,
vec![]
));
Comment on lines +1544 to +1551
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this end up terminating the contract?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you look at the code in fixtures/self_destruct.wat then you should find the information, that by passing empty input to thte contract, it will call seal_terminate. And as far as I can understand text wasm format it does so :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The contract terminates if called with an empty input.


assert!(!<ContractInfoOf::<Test>>::contains_key(&addr));
assert_matches!(child::get(trie, &[99]), Some(42));

tries.push(trie.clone())
}

// Run single lazy removal
Contracts::on_initialize(Weight::max_value());

// The single lazy removal should have removed all queued tries
for trie in tries.iter() {
assert_matches!(child::get::<i32>(trie, &[99]), None);
}
});
}

#[test]
fn lazy_removal_partial_remove_works() {
let (code, hash) = compile_module::<Test>("self_destruct").unwrap();
Expand Down
Loading