Skip to content

Commit 670966e

Browse files
ahmadkaouklibrelois
andcommitted
Storage Cleaner Precompile (polkadot-evm#1224)
* add precompile clear-storage skeleton * implement precompile clear storage * precompile storage cleaner * mock for testing * add some rust tests * add more rust tests * cover suicided contracts with no storage items * fix * fix bugs * fix precompile * Update precompile signature Co-authored-by: Éloïs <[email protected]> * fix precompile signature * add a limit * fix limit * Fix mock * update precompile * format Cargo.toml * rust fmt * fix clippy warnings * fix format * fix tests * refactor precompile and record weights for clear_suicided_contract * refactor tests * fix formatting * refactor to record cost at the begining * fix format * Fix clippy warnings * update Cargo.lock * Fix std feature * update dependencies * Fix clippy warnings --------- Co-authored-by: librelois <[email protected]>
1 parent 58e15d0 commit 670966e

File tree

7 files changed

+809
-1
lines changed

7 files changed

+809
-1
lines changed

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ members = [
1717
"frame/evm/precompile/bls12381",
1818
"frame/evm/precompile/dispatch",
1919
"frame/evm/precompile/curve25519",
20+
"frame/evm/precompile/storage-cleaner",
2021
"client/api",
2122
"client/consensus",
2223
"client/rpc-core",
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[package]
2+
name = "pallet-evm-precompile-storage-cleaner"
3+
version = "0.1.0"
4+
license = "Apache-2.0"
5+
description = "Storage cleaner precompile to clean storage of a suicided contracts"
6+
authors = { workspace = true }
7+
edition = { workspace = true }
8+
repository = { workspace = true }
9+
10+
[dependencies]
11+
scale-codec = { package = "parity-scale-codec", workspace = true }
12+
# Substrate
13+
frame-support = { workspace = true }
14+
frame-system = { workspace = true }
15+
sp-core = { workspace = true }
16+
sp-runtime = { workspace = true }
17+
sp-std = { workspace = true }
18+
# Frontier
19+
fp-evm = { workspace = true }
20+
pallet-evm = { workspace = true }
21+
precompile-utils = { workspace = true }
22+
23+
[dev-dependencies]
24+
scale-info = { workspace = true }
25+
# Substrate
26+
frame-system = { workspace = true, features = ["default"] }
27+
pallet-balances = { workspace = true, features = ["default", "insecure_zero_ed"] }
28+
pallet-timestamp = { workspace = true, features = ["default"] }
29+
pallet-utility = { workspace = true, features = ["default"] }
30+
rlp = { workspace = true }
31+
sp-core = { workspace = true, features = ["default"] }
32+
sp-io = { workspace = true, features = ["default"] }
33+
sp-runtime = { workspace = true, features = ["default"] }
34+
sp-std = { workspace = true, features = ["default"] }
35+
36+
# Frontier
37+
precompile-utils = { workspace = true, features = ["std", "testing"] }
38+
39+
[features]
40+
default = ["std"]
41+
std = [
42+
"scale-codec/std",
43+
# Substrate
44+
"frame-support/std",
45+
"frame-system/std",
46+
"sp-runtime/std",
47+
"sp-core/std",
48+
"sp-std/std",
49+
# Frontier
50+
"fp-evm/std",
51+
"pallet-evm/std",
52+
"precompile-utils/std",
53+
]
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// This file is part of Frontier.
3+
//
4+
// Copyright (c) 2020-2022 Parity Technologies (UK) Ltd.
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
18+
//! Storage cleaner precompile. This precompile is used to clean the storage entries of smart contract that
19+
//! has been marked as suicided (self-destructed).
20+
21+
#![cfg_attr(not(feature = "std"), no_std)]
22+
extern crate alloc;
23+
24+
use core::marker::PhantomData;
25+
use fp_evm::{PrecompileFailure, ACCOUNT_BASIC_PROOF_SIZE, ACCOUNT_STORAGE_PROOF_SIZE};
26+
use pallet_evm::AddressMapping;
27+
use precompile_utils::{prelude::*, EvmResult};
28+
use sp_core::H160;
29+
use sp_runtime::traits::ConstU32;
30+
use sp_std::vec::Vec;
31+
32+
#[cfg(test)]
33+
mod mock;
34+
#[cfg(test)]
35+
mod tests;
36+
37+
pub const ARRAY_LIMIT: u32 = 1_000;
38+
type GetArrayLimit = ConstU32<ARRAY_LIMIT>;
39+
// Storage key for suicided contracts: Blake2_128(16) + Key (H160(20))
40+
pub const SUICIDED_STORAGE_KEY: u64 = 36;
41+
42+
#[derive(Debug, Clone)]
43+
pub struct StorageCleanerPrecompile<Runtime>(PhantomData<Runtime>);
44+
45+
#[precompile_utils::precompile]
46+
impl<Runtime> StorageCleanerPrecompile<Runtime>
47+
where
48+
Runtime: pallet_evm::Config,
49+
{
50+
/// Clear Storage entries of smart contracts that has been marked as suicided (self-destructed). It takes a list of
51+
/// addresses and a limit as input. The limit is used to prevent the function from consuming too much gas. The
52+
/// maximum number of storage entries that can be removed is limit - 1.
53+
#[precompile::public("clearSuicidedStorage(address[],uint64)")]
54+
fn clear_suicided_storage(
55+
handle: &mut impl PrecompileHandle,
56+
addresses: BoundedVec<Address, GetArrayLimit>,
57+
limit: u64,
58+
) -> EvmResult {
59+
let addresses: Vec<_> = addresses.into();
60+
let nb_addresses = addresses.len() as u64;
61+
if limit == 0 {
62+
return Err(revert("Limit should be greater than zero"));
63+
}
64+
65+
Self::record_max_cost(handle, nb_addresses, limit)?;
66+
let result = Self::clear_suicided_storage_inner(addresses, limit - 1)?;
67+
Self::refund_cost(handle, result, nb_addresses, limit);
68+
69+
Ok(())
70+
}
71+
72+
/// This function iterates over the addresses, checks if each address is marked as suicided, and then deletes the storage
73+
/// entries associated with that address. If there are no remaining entries, we clear the suicided contract by calling the
74+
/// `clear_suicided_contract` function.
75+
fn clear_suicided_storage_inner(
76+
addresses: Vec<Address>,
77+
limit: u64,
78+
) -> Result<RemovalResult, PrecompileFailure> {
79+
let mut deleted_entries = 0u64;
80+
let mut deleted_contracts = 0u64;
81+
82+
for Address(address) in addresses {
83+
if !pallet_evm::Pallet::<Runtime>::is_account_suicided(&address) {
84+
return Err(revert(alloc::format!("NotSuicided: {}", address)));
85+
}
86+
87+
let deleted = pallet_evm::AccountStorages::<Runtime>::drain_prefix(address)
88+
.take((limit.saturating_sub(deleted_entries)) as usize)
89+
.count();
90+
deleted_entries = deleted_entries.saturating_add(deleted as u64);
91+
92+
// Check if the storage of this contract has been completly removed
93+
if pallet_evm::AccountStorages::<Runtime>::iter_key_prefix(address)
94+
.next()
95+
.is_none()
96+
{
97+
Self::clear_suicided_contract(address);
98+
deleted_contracts = deleted_contracts.saturating_add(1);
99+
}
100+
101+
if deleted_entries >= limit {
102+
break;
103+
}
104+
}
105+
106+
Ok(RemovalResult {
107+
deleted_entries,
108+
deleted_contracts,
109+
})
110+
}
111+
112+
/// Record the maximum cost (Worst case Scenario) of the clear_suicided_storage function.
113+
fn record_max_cost(
114+
handle: &mut impl PrecompileHandle,
115+
nb_addresses: u64,
116+
limit: u64,
117+
) -> EvmResult {
118+
let read_cost = RuntimeHelper::<Runtime>::db_read_gas_cost();
119+
let write_cost = RuntimeHelper::<Runtime>::db_write_gas_cost();
120+
let ref_time = 0u64
121+
// EVM:: Suicided (reads = nb_addresses)
122+
.saturating_add(read_cost.saturating_mul(nb_addresses))
123+
// EVM:: Suicided (writes = nb_addresses)
124+
.saturating_add(write_cost.saturating_mul(nb_addresses))
125+
// System: AccountInfo (reads = nb_addresses) for decrementing sufficients
126+
.saturating_add(read_cost.saturating_mul(nb_addresses))
127+
// System: AccountInfo (writes = nb_addresses) for decrementing sufficients
128+
.saturating_add(write_cost.saturating_mul(nb_addresses))
129+
// EVM: AccountStorage (reads = limit)
130+
.saturating_add(read_cost.saturating_mul(limit))
131+
// EVM: AccountStorage (writes = limit)
132+
.saturating_add(write_cost.saturating_mul(limit));
133+
134+
let proof_size = 0u64
135+
// Proof: EVM::Suicided (SUICIDED_STORAGE_KEY) * nb_addresses
136+
.saturating_add(SUICIDED_STORAGE_KEY.saturating_mul(nb_addresses))
137+
// Proof: EVM::AccountStorage (ACCOUNT_BASIC_PROOF_SIZE) * limit
138+
.saturating_add(ACCOUNT_STORAGE_PROOF_SIZE.saturating_mul(limit))
139+
// Proof: System::AccountInfo (ACCOUNT_BASIC_PROOF_SIZE) * nb_addresses
140+
.saturating_add(ACCOUNT_BASIC_PROOF_SIZE.saturating_mul(nb_addresses));
141+
142+
handle.record_external_cost(Some(ref_time), Some(proof_size), None)?;
143+
Ok(())
144+
}
145+
146+
/// Refund the additional cost recorded for the clear_suicided_storage function.
147+
fn refund_cost(
148+
handle: &mut impl PrecompileHandle,
149+
result: RemovalResult,
150+
nb_addresses: u64,
151+
limit: u64,
152+
) {
153+
let read_cost = RuntimeHelper::<Runtime>::db_read_gas_cost();
154+
let write_cost = RuntimeHelper::<Runtime>::db_write_gas_cost();
155+
156+
let extra_entries = limit.saturating_sub(result.deleted_entries);
157+
let extra_contracts = nb_addresses.saturating_sub(result.deleted_contracts);
158+
159+
let mut ref_time = 0u64;
160+
let mut proof_size = 0u64;
161+
162+
// Refund the cost of the remaining entries
163+
if extra_entries > 0 {
164+
ref_time = ref_time
165+
// EVM:: AccountStorage (reads = extra_entries)
166+
.saturating_add(read_cost.saturating_mul(extra_entries))
167+
// EVM:: AccountStorage (writes = extra_entries)
168+
.saturating_add(write_cost.saturating_mul(extra_entries));
169+
proof_size = proof_size
170+
// Proof: EVM::AccountStorage (ACCOUNT_BASIC_PROOF_SIZE) * extra_entries
171+
.saturating_add(ACCOUNT_STORAGE_PROOF_SIZE.saturating_mul(extra_entries));
172+
}
173+
174+
// Refund the cost of the remaining contracts
175+
if extra_contracts > 0 {
176+
ref_time = ref_time
177+
// EVM:: Suicided (reads = extra_contracts)
178+
.saturating_add(read_cost.saturating_mul(extra_contracts))
179+
// EVM:: Suicided (writes = extra_contracts)
180+
.saturating_add(write_cost.saturating_mul(extra_contracts))
181+
// System: AccountInfo (reads = extra_contracts) for decrementing sufficients
182+
.saturating_add(read_cost.saturating_mul(extra_contracts))
183+
// System: AccountInfo (writes = extra_contracts) for decrementing sufficients
184+
.saturating_add(write_cost.saturating_mul(extra_contracts));
185+
proof_size = proof_size
186+
// Proof: EVM::Suicided (SUICIDED_STORAGE_KEY) * extra_contracts
187+
.saturating_add(SUICIDED_STORAGE_KEY.saturating_mul(extra_contracts))
188+
// Proof: System::AccountInfo (ACCOUNT_BASIC_PROOF_SIZE) * extra_contracts
189+
.saturating_add(ACCOUNT_BASIC_PROOF_SIZE.saturating_mul(extra_contracts));
190+
}
191+
192+
handle.refund_external_cost(Some(ref_time), Some(proof_size));
193+
}
194+
195+
/// Clears the storage of a suicided contract.
196+
///
197+
/// This function will remove the given address from the list of suicided contracts
198+
/// and decrement the sufficients of the account associated with the address.
199+
fn clear_suicided_contract(address: H160) {
200+
pallet_evm::Suicided::<Runtime>::remove(address);
201+
202+
let account_id = Runtime::AddressMapping::into_account_id(address);
203+
let _ = frame_system::Pallet::<Runtime>::dec_sufficients(&account_id);
204+
}
205+
}
206+
207+
struct RemovalResult {
208+
pub deleted_entries: u64,
209+
pub deleted_contracts: u64,
210+
}

0 commit comments

Comments
 (0)