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

Commit 874794d

Browse files
pepyakinNikVolfathei
authored
pallet-contracts: State rent fixes (#6147)
* Don't store the storage size offset in the contract itself. * Clean the AccountDb code a bit * Use `storage_size: 0` when creating AliveContractInfo * Count empty storage items. * Update frame/contracts/src/account_db.rs Co-authored-by: Nikolay Volf <nikvolf@gmail.com> * Use more clear wording. Co-authored-by: Alexander Theißen <athei@users.noreply.github.com> * Change the order of decrement and increment for storage size Co-authored-by: Nikolay Volf <nikvolf@gmail.com> Co-authored-by: Alexander Theißen <athei@users.noreply.github.com>
1 parent df59acf commit 874794d

File tree

5 files changed

+209
-47
lines changed

5 files changed

+209
-47
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
;; This module stores a KV pair into the storage
2+
(module
3+
(import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32)))
4+
(import "env" "memory" (memory 16 16))
5+
6+
(func (export "call")
7+
)
8+
(func (export "deploy")
9+
(call $ext_set_storage
10+
(i32.const 0) ;; Pointer to storage key
11+
(i32.const 0) ;; Pointer to value
12+
(i32.load (i32.const 0)) ;; Size of value
13+
)
14+
)
15+
)

frame/contracts/src/account_db.rs

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use sp_std::collections::btree_map::{BTreeMap, Entry};
2626
use sp_std::prelude::*;
2727
use sp_io::hashing::blake2_256;
2828
use sp_runtime::traits::{Bounded, Zero};
29-
use frame_support::traits::{Currency, Get, Imbalance, SignedImbalance};
29+
use frame_support::traits::{Currency, Imbalance, SignedImbalance};
3030
use frame_support::{storage::child, StorageMap};
3131
use frame_system;
3232

@@ -108,7 +108,12 @@ pub trait AccountDb<T: Trait> {
108108
///
109109
/// Trie id is None iff account doesn't have an associated trie id in <ContractInfoOf<T>>.
110110
/// Because DirectAccountDb bypass the lookup for this association.
111-
fn get_storage(&self, account: &T::AccountId, trie_id: Option<&TrieId>, location: &StorageKey) -> Option<Vec<u8>>;
111+
fn get_storage(
112+
&self,
113+
account: &T::AccountId,
114+
trie_id: Option<&TrieId>,
115+
location: &StorageKey,
116+
) -> Option<Vec<u8>>;
112117
/// If account has an alive contract then return the code hash associated.
113118
fn get_code_hash(&self, account: &T::AccountId) -> Option<CodeHash<T>>;
114119
/// If account has an alive contract then return the rent allowance associated.
@@ -126,9 +131,10 @@ impl<T: Trait> AccountDb<T> for DirectAccountDb {
126131
&self,
127132
_account: &T::AccountId,
128133
trie_id: Option<&TrieId>,
129-
location: &StorageKey
134+
location: &StorageKey,
130135
) -> Option<Vec<u8>> {
131-
trie_id.and_then(|id| child::get_raw(&crate::child_trie_info(&id[..]), &blake2_256(location)))
136+
trie_id
137+
.and_then(|id| child::get_raw(&crate::child_trie_info(&id[..]), &blake2_256(location)))
132138
}
133139
fn get_code_hash(&self, account: &T::AccountId) -> Option<CodeHash<T>> {
134140
<ContractInfoOf<T>>::get(account).and_then(|i| i.as_alive().map(|i| i.code_hash))
@@ -176,24 +182,26 @@ impl<T: Trait> AccountDb<T> for DirectAccountDb {
176182
child::kill_storage(&info.child_trie_info());
177183
AliveContractInfo::<T> {
178184
code_hash,
179-
storage_size: T::StorageSizeOffset::get(),
185+
storage_size: 0,
186+
empty_pair_count: 0,
187+
total_pair_count: 0,
180188
trie_id: <T as Trait>::TrieIdGenerator::trie_id(&address),
181189
deduct_block: <frame_system::Module<T>>::block_number(),
182190
rent_allowance: <BalanceOf<T>>::max_value(),
183191
last_write: None,
184192
}
185193
}
186194
// New contract is being instantiated.
187-
(_, None, Some(code_hash)) => {
188-
AliveContractInfo::<T> {
189-
code_hash,
190-
storage_size: T::StorageSizeOffset::get(),
191-
trie_id: <T as Trait>::TrieIdGenerator::trie_id(&address),
192-
deduct_block: <frame_system::Module<T>>::block_number(),
193-
rent_allowance: <BalanceOf<T>>::max_value(),
194-
last_write: None,
195-
}
196-
}
195+
(_, None, Some(code_hash)) => AliveContractInfo::<T> {
196+
code_hash,
197+
storage_size: 0,
198+
empty_pair_count: 0,
199+
total_pair_count: 0,
200+
trie_id: <T as Trait>::TrieIdGenerator::trie_id(&address),
201+
deduct_block: <frame_system::Module<T>>::block_number(),
202+
rent_allowance: <BalanceOf<T>>::max_value(),
203+
last_write: None,
204+
},
197205
// There is no existing at the address nor a new one to be instantiated.
198206
(_, None, None) => continue,
199207
};
@@ -210,18 +218,69 @@ impl<T: Trait> AccountDb<T> for DirectAccountDb {
210218
new_info.last_write = Some(<frame_system::Module<T>>::block_number());
211219
}
212220

213-
for (k, v) in changed.storage.into_iter() {
214-
if let Some(value) = child::get_raw(
215-
&new_info.child_trie_info(),
216-
&blake2_256(&k),
217-
) {
218-
new_info.storage_size -= value.len() as u32;
221+
// NB: this call allocates internally. To keep allocations to the minimum we cache
222+
// the child trie info here.
223+
let child_trie_info = new_info.child_trie_info();
224+
225+
// Here we iterate over all storage key-value pairs that were changed throughout the
226+
// execution of a contract and apply them to the substrate storage.
227+
for (key, opt_new_value) in changed.storage.into_iter() {
228+
let hashed_key = blake2_256(&key);
229+
230+
// In order to correctly update the book keeping we need to fetch the previous
231+
// value of the key-value pair.
232+
//
233+
// It might be a bit more clean if we had an API that supported getting the size
234+
// of the value without going through the loading of it. But at the moment of
235+
// writing, there is no such API.
236+
//
237+
// That's not a show stopper in any case, since the performance cost is
238+
// dominated by the trie traversal anyway.
239+
let opt_prev_value = child::get_raw(&child_trie_info, &hashed_key);
240+
241+
// Update the total number of KV pairs and the number of empty pairs.
242+
match (&opt_prev_value, &opt_new_value) {
243+
(Some(prev_value), None) => {
244+
new_info.total_pair_count -= 1;
245+
if prev_value.is_empty() {
246+
new_info.empty_pair_count -= 1;
247+
}
248+
},
249+
(None, Some(new_value)) => {
250+
new_info.total_pair_count += 1;
251+
if new_value.is_empty() {
252+
new_info.empty_pair_count += 1;
253+
}
254+
},
255+
(Some(prev_value), Some(new_value)) => {
256+
if prev_value.is_empty() {
257+
new_info.empty_pair_count -= 1;
258+
}
259+
if new_value.is_empty() {
260+
new_info.empty_pair_count += 1;
261+
}
262+
}
263+
(None, None) => {}
219264
}
220-
if let Some(value) = v {
221-
new_info.storage_size += value.len() as u32;
222-
child::put_raw(&new_info.child_trie_info(), &blake2_256(&k), &value[..]);
223-
} else {
224-
child::kill(&new_info.child_trie_info(), &blake2_256(&k));
265+
266+
// Update the total storage size.
267+
let prev_value_len = opt_prev_value
268+
.as_ref()
269+
.map(|old_value| old_value.len() as u32)
270+
.unwrap_or(0);
271+
let new_value_len = opt_new_value
272+
.as_ref()
273+
.map(|new_value| new_value.len() as u32)
274+
.unwrap_or(0);
275+
new_info.storage_size = new_info
276+
.storage_size
277+
.saturating_add(new_value_len)
278+
.saturating_sub(prev_value_len);
279+
280+
// Finally, perform the change on the storage.
281+
match opt_new_value {
282+
Some(new_value) => child::put_raw(&child_trie_info, &hashed_key, &new_value[..]),
283+
None => child::kill(&child_trie_info, &hashed_key),
225284
}
226285
}
227286

@@ -239,12 +298,14 @@ impl<T: Trait> AccountDb<T> for DirectAccountDb {
239298
// then it's indicative of a buggy contracts system.
240299
// Panicking is far from ideal as it opens up a DoS attack on block validators, however
241300
// it's a less bad option than allowing arbitrary value to be created.
242-
SignedImbalance::Positive(ref p) if !p.peek().is_zero() =>
243-
panic!("contract subsystem resulting in positive imbalance!"),
301+
SignedImbalance::Positive(ref p) if !p.peek().is_zero() => {
302+
panic!("contract subsystem resulting in positive imbalance!")
303+
}
244304
_ => {}
245305
}
246306
}
247307
}
308+
248309
pub struct OverlayAccountDb<'a, T: Trait + 'a> {
249310
local: RefCell<ChangeSet<T>>,
250311
underlying: &'a dyn AccountDb<T>,
@@ -267,7 +328,8 @@ impl<'a, T: Trait> OverlayAccountDb<'a, T> {
267328
location: StorageKey,
268329
value: Option<Vec<u8>>,
269330
) {
270-
self.local.borrow_mut()
331+
self.local
332+
.borrow_mut()
271333
.entry(account.clone())
272334
.or_insert(Default::default())
273335
.storage
@@ -285,7 +347,7 @@ impl<'a, T: Trait> OverlayAccountDb<'a, T> {
285347
}
286348

287349
let mut local = self.local.borrow_mut();
288-
let contract = local.entry(account.clone()).or_insert_with(|| Default::default());
350+
let contract = local.entry(account.clone()).or_default();
289351

290352
contract.code_hash = Some(code_hash);
291353
contract.rent_allowance = Some(<BalanceOf<T>>::max_value());
@@ -301,7 +363,7 @@ impl<'a, T: Trait> OverlayAccountDb<'a, T> {
301363
ChangeEntry {
302364
reset: true,
303365
..Default::default()
304-
}
366+
},
305367
);
306368
}
307369

@@ -327,7 +389,7 @@ impl<'a, T: Trait> AccountDb<T> for OverlayAccountDb<'a, T> {
327389
&self,
328390
account: &T::AccountId,
329391
trie_id: Option<&TrieId>,
330-
location: &StorageKey
392+
location: &StorageKey,
331393
) -> Option<Vec<u8>> {
332394
self.local
333395
.borrow()

frame/contracts/src/lib.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,15 @@ pub type AliveContractInfo<T> =
203203
pub struct RawAliveContractInfo<CodeHash, Balance, BlockNumber> {
204204
/// Unique ID for the subtree encoded as a bytes vector.
205205
pub trie_id: TrieId,
206-
/// The size of stored value in octet.
206+
/// The total number of bytes used by this contract.
207+
///
208+
/// It is a sum of each key-value pair stored by this contract.
207209
pub storage_size: u32,
210+
/// The number of key-value pairs that have values of zero length.
211+
/// The condition `empty_pair_count ≤ total_pair_count` always holds.
212+
pub empty_pair_count: u32,
213+
/// The total number of key-value pairs in storage of this contract.
214+
pub total_pair_count: u32,
208215
/// The code associated with a given account.
209216
pub code_hash: CodeHash,
210217
/// Pay rent at most up to this value.
@@ -338,8 +345,11 @@ pub trait Trait: frame_system::Trait + pallet_transaction_payment::Trait {
338345
/// The minimum amount required to generate a tombstone.
339346
type TombstoneDeposit: Get<BalanceOf<Self>>;
340347

341-
/// Size of a contract at the time of instantiation. This is a simple way to ensure
342-
/// that empty contracts eventually gets deleted.
348+
/// A size offset for an contract. A just created account with untouched storage will have that
349+
/// much of storage from the perspective of the state rent.
350+
///
351+
/// This is a simple way to ensure that contracts with empty storage eventually get deleted by
352+
/// making them pay rent. This creates an incentive to remove them early in order to save rent.
343353
type StorageSizeOffset: Get<u32>;
344354

345355
/// Price of a byte of storage per one block interval. Should be greater than 0.
@@ -420,8 +430,12 @@ decl_module! {
420430
/// The minimum amount required to generate a tombstone.
421431
const TombstoneDeposit: BalanceOf<T> = T::TombstoneDeposit::get();
422432

423-
/// Size of a contract at the time of instantiation. This is a simple way to ensure that
424-
/// empty contracts eventually gets deleted.
433+
/// A size offset for an contract. A just created account with untouched storage will have that
434+
/// much of storage from the perspective of the state rent.
435+
///
436+
/// This is a simple way to ensure that contracts with empty storage eventually get deleted
437+
/// by making them pay rent. This creates an incentive to remove them early in order to save
438+
/// rent.
425439
const StorageSizeOffset: u32 = T::StorageSizeOffset::get();
426440

427441
/// Price of a byte of storage per one block interval. Should be greater than 0.
@@ -697,7 +711,7 @@ impl<T: Trait> Module<T> {
697711
dest: T::AccountId,
698712
code_hash: CodeHash<T>,
699713
rent_allowance: BalanceOf<T>,
700-
delta: Vec<exec::StorageKey>
714+
delta: Vec<exec::StorageKey>,
701715
) -> DispatchResult {
702716
let mut origin_contract = <ContractInfoOf<T>>::get(&origin)
703717
.and_then(|c| c.get_alive())
@@ -764,6 +778,8 @@ impl<T: Trait> Module<T> {
764778
<ContractInfoOf<T>>::insert(&dest, ContractInfo::Alive(RawAliveContractInfo {
765779
trie_id: origin_contract.trie_id,
766780
storage_size: origin_contract.storage_size,
781+
empty_pair_count: origin_contract.empty_pair_count,
782+
total_pair_count: origin_contract.total_pair_count,
767783
code_hash,
768784
rent_allowance,
769785
deduct_block: current_block,

frame/contracts/src/rent.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,13 @@ fn compute_fee_per_block<T: Trait>(
9292
.checked_div(&T::RentDepositOffset::get())
9393
.unwrap_or_else(Zero::zero);
9494

95-
let effective_storage_size =
96-
<BalanceOf<T>>::from(contract.storage_size).saturating_sub(free_storage);
95+
// For now, we treat every empty KV pair as if it was one byte long.
96+
let empty_pairs_equivalent = contract.empty_pair_count;
97+
98+
let effective_storage_size = <BalanceOf<T>>::from(
99+
contract.storage_size + T::StorageSizeOffset::get() + empty_pairs_equivalent,
100+
)
101+
.saturating_sub(free_storage);
97102

98103
effective_storage_size
99104
.checked_mul(&T::RentByteFee::get())

0 commit comments

Comments
 (0)