contracts: Add storage deposits (#10082)

* Frame no longer needs to be mutable (refactoring artifact)

* Remove Contract/Tombstone deposit

* Add StorageMeter

* cargo fmt

* Fix weight annotation

* cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Simplify keep check for contract accounts

- Make sure that the "base deposit" for each contract >= ed
- Remove now obsolete checks when sneding away free balance

* Remove unused imports and functions

* Rename storage_limit to storage_deposit_limit

* cargo fmt

* Fix typo

Co-authored-by: Michael Müller <michi@parity.io>

* Finish up rename of storage_limit

* Fix rpc tests

* Make use of `StorageDepositLimitTooHigh`

* Add tests and fix bugs discovered by tests

* Add storage migration

* Don't use u128 in RPC

* Fix weight of migration

* Rename `endowment` to `value`

* Fix bug where contract couldn't get funded by a storage deposit

- Make sure that contract gets funded from deposits before value is transferred
- Don't reserve value at origin because otherwise funding isn't possible
	- Just transfer free balance and reserve it after the transfer
- When refunding make sure that this refund can't dust the contract
	- Can only happen after a runtime upgrade where costs where upped
- Add more tests

* Apply suggestions from code review

Co-authored-by: Andrew Jones <ascjones@gmail.com>

* Remove unused `fn storage_meter`

* Fix copy pasta doc error

* Import `MaxEncodeLen` from codec

* Beautify RPC trait bounds

* Add re-instrument behaviour to dispatchable doc

* Make sure a account won't be destroyed a refund after a slash

* Apply suggestions from code review

Co-authored-by: Andrew Jones <ascjones@gmail.com>

* Update `Storage::write` docs

* Improve doc

* Remove superflous conditional

* Typos

* Remove superflous clone (refactoring artifact)

* Apply suggestions from code review

Co-authored-by: Andrew Jones <ascjones@gmail.com>

Co-authored-by: Parity Bot <admin@parity.io>
Co-authored-by: Michael Müller <michi@parity.io>
Co-authored-by: Andrew Jones <ascjones@gmail.com>
This commit is contained in:
Alexander Theißen
2021-12-07 13:19:30 +01:00
committed by GitHub
parent 56fb1cfbb6
commit 6863476603
27 changed files with 4395 additions and 1914 deletions
+116 -131
View File
@@ -28,177 +28,170 @@
//! this guarantees that every instrumented contract code in cache cannot have the version equal to
//! the current one. Thus, before executing a contract it should be reinstrument with new schedule.
#[cfg(feature = "runtime-benchmarks")]
pub use self::private::reinstrument;
use crate::{
gas::{GasMeter, Token},
wasm::{prepare, PrefabWasmModule},
weights::WeightInfo,
CodeHash, CodeStorage, Config, Error, Event, Pallet as Contracts, PristineCode, Schedule,
CodeHash, CodeStorage, Config, Error, Event, OwnerInfoOf, Pallet, PristineCode, Schedule,
Weight,
};
use frame_support::dispatch::DispatchError;
use frame_support::{
dispatch::{DispatchError, DispatchResult},
ensure,
storage::StorageMap,
traits::ReservableCurrency,
};
use sp_core::crypto::UncheckedFrom;
use sp_runtime::traits::BadOrigin;
/// Put the instrumented module in storage.
///
/// Increments the refcount of the in-storage `prefab_module` if it already exists in storage
/// under the specified `code_hash`.
pub fn store<T: Config>(mut prefab_module: PrefabWasmModule<T>)
pub fn store<T: Config>(mut module: PrefabWasmModule<T>, instantiated: bool) -> DispatchResult
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
let code_hash = sp_std::mem::take(&mut prefab_module.code_hash);
// original_code is only `Some` if the contract was instantiated from a new code
// but `None` if it was loaded from storage.
if let Some(code) = prefab_module.original_code.take() {
<PristineCode<T>>::insert(&code_hash, code);
}
let code_hash = sp_std::mem::take(&mut module.code_hash);
<CodeStorage<T>>::mutate(&code_hash, |existing| match existing {
Some(module) => increment_64(&mut module.refcount),
None => {
*existing = Some(prefab_module);
Contracts::<T>::deposit_event(Event::CodeStored { code_hash })
},
});
}
/// Increment the refcount of a code in-storage by one.
pub fn increment_refcount<T: Config>(
code_hash: CodeHash<T>,
gas_meter: &mut GasMeter<T>,
) -> Result<(), DispatchError>
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
gas_meter.charge(CodeToken::UpdateRefcount(estimate_code_size::<T>(&code_hash)?))?;
<CodeStorage<T>>::mutate(code_hash, |existing| {
if let Some(module) = existing {
increment_64(&mut module.refcount);
Some(existing) => {
// We instrument any uploaded contract anyways. We might as well store it to save
// a potential re-instrumentation later.
existing.code = module.code;
existing.instruction_weights_version = module.instruction_weights_version;
// When the code was merely uploaded but not instantiated we can skip this.
if instantiated {
<OwnerInfoOf<T>>::mutate(&code_hash, |owner_info| {
if let Some(owner_info) = owner_info {
owner_info.refcount = owner_info.refcount.checked_add(1).expect(
"
refcount is 64bit. Generating this overflow would require to store
_at least_ 18 exabyte of data assuming that a contract consumes only
one byte of data. Any node would run out of storage space before hitting
this overflow.
qed
",
);
}
})
}
Ok(())
} else {
Err(Error::<T>::CodeNotFound.into())
}
},
None => {
let orig_code = module.original_code.take().expect(
"
If an executable isn't in storage it was uploaded.
If it was uploaded the original code must exist. qed
",
);
let mut owner_info = module.owner_info.take().expect(
"If an executable isn't in storage it was uploaded.
If it was uploaded the owner info was generated and attached. qed
",
);
// This `None` case happens only in freshly uploaded modules. This means that
// the `owner` is always the origin of the current transaction.
T::Currency::reserve(&owner_info.owner, owner_info.deposit)
.map_err(|_| <Error<T>>::StorageDepositNotEnoughFunds)?;
owner_info.refcount = if instantiated { 1 } else { 0 };
<PristineCode<T>>::insert(&code_hash, orig_code);
<OwnerInfoOf<T>>::insert(&code_hash, owner_info);
*existing = Some(module);
<Pallet<T>>::deposit_event(Event::CodeStored { code_hash });
Ok(())
},
})
}
/// Decrement the refcount of a code in-storage by one and remove the code when it drops to zero.
pub fn decrement_refcount<T: Config>(
code_hash: CodeHash<T>,
gas_meter: &mut GasMeter<T>,
) -> Result<(), DispatchError>
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
if let Ok(len) = estimate_code_size::<T>(&code_hash) {
gas_meter.charge(CodeToken::UpdateRefcount(len))?;
}
<CodeStorage<T>>::mutate_exists(code_hash, |existing| {
if let Some(module) = existing {
module.refcount = module.refcount.saturating_sub(1);
if module.refcount == 0 {
*existing = None;
finish_removal::<T>(code_hash);
}
/// Decrement the refcount of a code in-storage by one.
///
/// # Note
///
/// A contract whose refcount dropped to zero isn't automatically removed. A `remove_code`
/// transaction must be submitted by the original uploader to do so.
pub fn decrement_refcount<T: Config>(code_hash: CodeHash<T>) -> Result<(), DispatchError> {
<OwnerInfoOf<T>>::mutate(code_hash, |existing| {
if let Some(info) = existing {
info.refcount = info.refcount.saturating_sub(1);
}
});
Ok(())
}
/// Try to remove code together with all associated information.
pub fn try_remove<T: Config>(origin: &T::AccountId, code_hash: CodeHash<T>) -> DispatchResult {
<OwnerInfoOf<T>>::try_mutate_exists(&code_hash, |existing| {
if let Some(owner_info) = existing {
ensure!(owner_info.refcount == 0, <Error<T>>::CodeInUse);
ensure!(&owner_info.owner == origin, BadOrigin);
T::Currency::unreserve(&owner_info.owner, owner_info.deposit);
*existing = None;
<PristineCode<T>>::remove(&code_hash);
<CodeStorage<T>>::remove(&code_hash);
<Pallet<T>>::deposit_event(Event::CodeRemoved { code_hash });
Ok(())
} else {
Err(<Error<T>>::CodeNotFound.into())
}
})
}
/// Load code with the given code hash.
///
/// If the module was instrumented with a lower version of schedule than
/// the current one given as an argument, then this function will perform
/// re-instrumentation and update the cache in the storage.
///
/// # Note
///
/// If `reinstrument` is set it is assumed that the load is performed in the context of
/// a contract call: This means we charge the size based cased for loading the contract.
pub fn load<T: Config>(
code_hash: CodeHash<T>,
mut reinstrument: Option<(&Schedule<T>, &mut GasMeter<T>)>,
schedule: &Schedule<T>,
gas_meter: &mut GasMeter<T>,
) -> Result<PrefabWasmModule<T>, DispatchError>
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
// The reinstrument case coincides with the cases where we need to charge extra
// based upon the code size: On-chain execution.
if let Some((_, gas_meter)) = &mut reinstrument {
gas_meter.charge(CodeToken::Load(estimate_code_size::<T>(&code_hash)?))?;
}
gas_meter.charge(CodeToken::Load(estimate_code_size::<T, CodeStorage<T>, _>(&code_hash)?))?;
let mut prefab_module =
<CodeStorage<T>>::get(code_hash).ok_or_else(|| Error::<T>::CodeNotFound)?;
prefab_module.code_hash = code_hash;
if let Some((schedule, gas_meter)) = reinstrument {
if prefab_module.instruction_weights_version < schedule.instruction_weights.version {
// The instruction weights have changed.
// We need to re-instrument the code with the new instruction weights.
gas_meter.charge(CodeToken::Instrument(prefab_module.original_code_len))?;
private::reinstrument(&mut prefab_module, schedule)?;
}
if prefab_module.instruction_weights_version < schedule.instruction_weights.version {
// The instruction weights have changed.
// We need to re-instrument the code with the new instruction weights.
gas_meter.charge(CodeToken::Instrument(estimate_code_size::<T, PristineCode<T>, _>(
&code_hash,
)?))?;
reinstrument(&mut prefab_module, schedule)?;
}
Ok(prefab_module)
}
mod private {
use super::*;
/// Instruments the passed prefab wasm module with the supplied schedule.
pub fn reinstrument<T: Config>(
prefab_module: &mut PrefabWasmModule<T>,
schedule: &Schedule<T>,
) -> Result<(), DispatchError>
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
let original_code = <PristineCode<T>>::get(&prefab_module.code_hash)
.ok_or_else(|| Error::<T>::CodeNotFound)?;
prefab_module.code = prepare::reinstrument_contract::<T>(original_code, schedule)?;
prefab_module.instruction_weights_version = schedule.instruction_weights.version;
<CodeStorage<T>>::insert(&prefab_module.code_hash, &*prefab_module);
Ok(())
}
/// Instruments the passed prefab wasm module with the supplied schedule.
pub fn reinstrument<T: Config>(
prefab_module: &mut PrefabWasmModule<T>,
schedule: &Schedule<T>,
) -> Result<(), DispatchError> {
let original_code =
<PristineCode<T>>::get(&prefab_module.code_hash).ok_or_else(|| Error::<T>::CodeNotFound)?;
prefab_module.code = prepare::reinstrument_contract::<T>(original_code, schedule)?;
prefab_module.instruction_weights_version = schedule.instruction_weights.version;
<CodeStorage<T>>::insert(&prefab_module.code_hash, &*prefab_module);
Ok(())
}
/// Finish removal of a code by deleting the pristine code and emitting an event.
fn finish_removal<T: Config>(code_hash: CodeHash<T>)
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
<PristineCode<T>>::remove(code_hash);
Contracts::<T>::deposit_event(Event::CodeRemoved { code_hash })
}
/// Increment the refcount panicking if it should ever overflow (which will not happen).
/// Get the size of the code stored at `code_hash` without loading it.
///
/// We try hard to be infallible here because otherwise more storage transactions would be
/// necessary to account for failures in storing code for an already instantiated contract.
fn increment_64(refcount: &mut u64) {
*refcount = refcount.checked_add(1).expect(
"
refcount is 64bit. Generating this overflow would require to store
_at least_ 18 exabyte of data assuming that a contract consumes only
one byte of data. Any node would run out of storage space before hitting
this overflow.
qed
",
);
}
/// Get the size of the instrumented code stored at `code_hash` without loading it.
///
/// The returned value is slightly too large because it also contains the fields apart from
/// `code` which are located inside [`PrefabWasmModule`]. However, those are negligible when
/// compared to the code size. Additionally, charging too much weight is completely safe.
fn estimate_code_size<T: Config>(code_hash: &CodeHash<T>) -> Result<u32, DispatchError>
/// The returned value is slightly too large when using it for the [`PrefabWasmModule`]
/// because it has other fields in addition to the code itself. However, those are negligible
/// when compared to the code size. Additionally, charging too much weight is completely safe.
fn estimate_code_size<T, M, V>(code_hash: &CodeHash<T>) -> Result<u32, DispatchError>
where
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
T: Config,
M: StorageMap<CodeHash<T>, V>,
V: codec::FullCodec,
{
let key = <CodeStorage<T>>::hashed_key_for(code_hash);
let key = M::hashed_key_for(code_hash);
let mut data = [0u8; 0];
let len = sp_io::storage::read(&key, &mut data, 0).ok_or_else(|| Error::<T>::CodeNotFound)?;
Ok(len)
@@ -212,18 +205,12 @@ enum CodeToken {
Instrument(u32),
/// Weight for loading a contract per kilobyte.
Load(u32),
/// Weight for changing the refcount of a contract per kilobyte.
UpdateRefcount(u32),
}
impl<T> Token<T> for CodeToken
where
T: Config,
T::AccountId: UncheckedFrom<T::Hash> + AsRef<[u8]>,
{
impl<T: Config> Token<T> for CodeToken {
fn weight(&self) -> Weight {
use self::CodeToken::*;
// In case of `Load` and `UpdateRefcount` we already covered the general costs of
// In case of `Load` we already covered the general costs of
// accessing the storage but still need to account for the actual size of the
// contract code. This is why we substract `T::*::(0)`. We need to do this at this
// point because when charging the general weight we do not know the size of
@@ -232,8 +219,6 @@ where
Instrument(len) => T::WeightInfo::instrument(len / 1024),
Load(len) =>
T::WeightInfo::code_load(len / 1024).saturating_sub(T::WeightInfo::code_load(0)),
UpdateRefcount(len) => T::WeightInfo::code_refcount(len / 1024)
.saturating_sub(T::WeightInfo::code_refcount(0)),
}
}
}