contracts: Consider contract size in weights (#8086)

* contracts: Consider contract size in weights

* Bump spec version

* Whitespace fix

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>

* Correct pre-charged code weight even in the error case

* Use the instrumented code size in weight calculation

* Charge the cost of re-instrumentation from the gas meter

* Fix benchmark

* cargo run --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

* Better documentation of return types

Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com>
Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
This commit is contained in:
Alexander Theißen
2021-02-22 09:52:58 +01:00
committed by GitHub
parent fbd3148bba
commit 84071d6d49
13 changed files with 1267 additions and 843 deletions
+124 -53
View File
@@ -72,8 +72,13 @@ pub trait Ext {
/// Instantiate a contract from the given code.
///
/// Returns the original code size of the called contract.
/// The newly created account will be associated with `code`. `value` specifies the amount of value
/// transferred from this to the newly created account (also known as endowment).
///
/// # Return Value
///
/// Result<(AccountId, ExecReturnValue, CodeSize), (ExecError, CodeSize)>
fn instantiate(
&mut self,
code: CodeHash<Self::T>,
@@ -81,7 +86,7 @@ pub trait Ext {
gas_meter: &mut GasMeter<Self::T>,
input_data: Vec<u8>,
salt: &[u8],
) -> Result<(AccountIdOf<Self::T>, ExecReturnValue), ExecError>;
) -> Result<(AccountIdOf<Self::T>, ExecReturnValue, u32), (ExecError, u32)>;
/// Transfer some amount of funds into the specified account.
fn transfer(
@@ -92,24 +97,35 @@ pub trait Ext {
/// Transfer all funds to `beneficiary` and delete the contract.
///
/// Returns the original code size of the terminated contract.
/// Since this function removes the self contract eagerly, if succeeded, no further actions should
/// be performed on this `Ext` instance.
///
/// This function will fail if the same contract is present on the contract
/// call stack.
///
/// # Return Value
///
/// Result<CodeSize, (DispatchError, CodeSize)>
fn terminate(
&mut self,
beneficiary: &AccountIdOf<Self::T>,
) -> DispatchResult;
) -> Result<u32, (DispatchError, u32)>;
/// Call (possibly transferring some amount of funds) into the specified account.
///
/// Returns the original code size of the called contract.
///
/// # Return Value
///
/// Result<(ExecReturnValue, CodeSize), (ExecError, CodeSize)>
fn call(
&mut self,
to: &AccountIdOf<Self::T>,
value: BalanceOf<Self::T>,
gas_meter: &mut GasMeter<Self::T>,
input_data: Vec<u8>,
) -> ExecResult;
) -> Result<(ExecReturnValue, u32), (ExecError, u32)>;
/// Restores the given destination contract sacrificing the current one.
///
@@ -118,13 +134,17 @@ pub trait Ext {
///
/// This function will fail if the same contract is present
/// on the contract call stack.
///
/// # Return Value
///
/// Result<(CallerCodeSize, DestCodeSize), (DispatchError, CallerCodeSize, DestCodesize)>
fn restore_to(
&mut self,
dest: AccountIdOf<Self::T>,
code_hash: CodeHash<Self::T>,
rent_allowance: BalanceOf<Self::T>,
delta: Vec<StorageKey>,
) -> DispatchResult;
) -> Result<(u32, u32), (DispatchError, u32, u32)>;
/// Returns a reference to the account id of the caller.
fn caller(&self) -> &AccountIdOf<Self::T>;
@@ -190,7 +210,11 @@ pub enum ExportedFunction {
/// order to be able to mock the wasm logic for testing.
pub trait Executable<T: Config>: Sized {
/// Load the executable from storage.
fn from_storage(code_hash: CodeHash<T>, schedule: &Schedule<T>) -> Result<Self, DispatchError>;
fn from_storage(
code_hash: CodeHash<T>,
schedule: &Schedule<T>,
gas_meter: &mut GasMeter<T>,
) -> Result<Self, DispatchError>;
/// Load the module from storage without re-instrumenting it.
///
@@ -203,10 +227,14 @@ pub trait Executable<T: Config>: Sized {
fn drop_from_storage(self);
/// Increment the refcount by one. Fails if the code does not exist on-chain.
fn add_user(code_hash: CodeHash<T>) -> DispatchResult;
///
/// Returns the size of the original code.
fn add_user(code_hash: CodeHash<T>) -> Result<u32, DispatchError>;
/// Decrement the refcount by one and remove the code when it drops to zero.
fn remove_user(code_hash: CodeHash<T>);
///
/// Returns the size of the original code.
fn remove_user(code_hash: CodeHash<T>) -> u32;
/// Execute the specified exported function and return the result.
///
@@ -238,6 +266,9 @@ pub trait Executable<T: Config>: Sized {
/// without refetching this from storage the result can be inaccurate as it might be
/// working with a stale value. Usually this inaccuracy is tolerable.
fn occupied_storage(&self) -> u32;
/// Size of the instrumented code in bytes.
fn code_len(&self) -> u32;
}
pub struct ExecutionContext<'a, T: Config + 'a, E> {
@@ -290,35 +321,42 @@ where
}
/// Make a call to the specified address, optionally transferring some funds.
///
/// # Return Value
///
/// Result<(ExecReturnValue, CodeSize), (ExecError, CodeSize)>
pub fn call(
&mut self,
dest: T::AccountId,
value: BalanceOf<T>,
gas_meter: &mut GasMeter<T>,
input_data: Vec<u8>,
) -> ExecResult {
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
if self.depth == T::MaxDepth::get() as usize {
Err(Error::<T>::MaxCallDepthReached)?
return Err((Error::<T>::MaxCallDepthReached.into(), 0));
}
let contract = <ContractInfoOf<T>>::get(&dest)
.and_then(|contract| contract.get_alive())
.ok_or(Error::<T>::NotCallable)?;
.ok_or((Error::<T>::NotCallable.into(), 0))?;
let executable = E::from_storage(contract.code_hash, &self.schedule)?;
let executable = E::from_storage(contract.code_hash, &self.schedule, gas_meter)
.map_err(|e| (e.into(), 0))?;
let code_len = executable.code_len();
// This charges the rent and denies access to a contract that is in need of
// eviction by returning `None`. We cannot evict eagerly here because those
// changes would be rolled back in case this contract is called by another
// contract.
// See: https://github.com/paritytech/substrate/issues/6439#issuecomment-648754324
let contract = Rent::<T, E>::charge(&dest, contract, executable.occupied_storage())?
.ok_or(Error::<T>::NotCallable)?;
let contract = Rent::<T, E>::charge(&dest, contract, executable.occupied_storage())
.map_err(|e| (e.into(), code_len))?
.ok_or((Error::<T>::NotCallable.into(), code_len))?;
let transactor_kind = self.transactor_kind();
let caller = self.self_account.clone();
self.with_nested_context(dest.clone(), contract.trie_id.clone(), |nested| {
let result = self.with_nested_context(dest.clone(), contract.trie_id.clone(), |nested| {
if value > BalanceOf::<T>::zero() {
transfer::<T>(
TransferCause::Call,
@@ -336,7 +374,8 @@ where
gas_meter,
).map_err(|e| ExecError { error: e.error, origin: ErrorOrigin::Callee })?;
Ok(output)
})
}).map_err(|e| (e, code_len))?;
Ok((result, code_len))
}
pub fn instantiate(
@@ -581,10 +620,13 @@ where
gas_meter: &mut GasMeter<T>,
input_data: Vec<u8>,
salt: &[u8],
) -> Result<(AccountIdOf<T>, ExecReturnValue), ExecError> {
let executable = E::from_storage(code_hash, &self.ctx.schedule)?;
let result = self.ctx.instantiate(endowment, gas_meter, executable, input_data, salt)?;
Ok(result)
) -> Result<(AccountIdOf<T>, ExecReturnValue, u32), (ExecError, u32)> {
let executable = E::from_storage(code_hash, &self.ctx.schedule, gas_meter)
.map_err(|e| (e.into(), 0))?;
let code_len = executable.code_len();
self.ctx.instantiate(endowment, gas_meter, executable, input_data, salt)
.map(|r| (r.0, r.1, code_len))
.map_err(|e| (e, code_len))
}
fn transfer(
@@ -604,12 +646,12 @@ where
fn terminate(
&mut self,
beneficiary: &AccountIdOf<Self::T>,
) -> DispatchResult {
) -> Result<u32, (DispatchError, u32)> {
let self_id = self.ctx.self_account.clone();
let value = T::Currency::free_balance(&self_id);
if let Some(caller_ctx) = self.ctx.caller {
if caller_ctx.is_live(&self_id) {
return Err(Error::<T>::ReentranceDenied.into());
return Err((Error::<T>::ReentranceDenied.into(), 0));
}
}
transfer::<T>(
@@ -618,12 +660,12 @@ where
&self_id,
beneficiary,
value,
)?;
).map_err(|e| (e, 0))?;
if let Some(ContractInfo::Alive(info)) = ContractInfoOf::<T>::take(&self_id) {
Storage::<T>::queue_trie_for_deletion(&info)?;
E::remove_user(info.code_hash);
Storage::<T>::queue_trie_for_deletion(&info).map_err(|e| (e, 0))?;
let code_len = E::remove_user(info.code_hash);
Contracts::<T>::deposit_event(RawEvent::Terminated(self_id, beneficiary.clone()));
Ok(())
Ok(code_len)
} else {
panic!(
"this function is only invoked by in the context of a contract;\
@@ -639,7 +681,7 @@ where
value: BalanceOf<T>,
gas_meter: &mut GasMeter<T>,
input_data: Vec<u8>,
) -> ExecResult {
) -> Result<(ExecReturnValue, u32), (ExecError, u32)> {
self.ctx.call(to.clone(), value, gas_meter, input_data)
}
@@ -649,10 +691,10 @@ where
code_hash: CodeHash<Self::T>,
rent_allowance: BalanceOf<Self::T>,
delta: Vec<StorageKey>,
) -> DispatchResult {
) -> Result<(u32, u32), (DispatchError, u32, u32)> {
if let Some(caller_ctx) = self.ctx.caller {
if caller_ctx.is_live(&self.ctx.self_account) {
return Err(Error::<T>::ReentranceDenied.into());
return Err((Error::<T>::ReentranceDenied.into(), 0, 0));
}
}
@@ -828,7 +870,8 @@ mod tests {
impl Executable<Test> for MockExecutable {
fn from_storage(
code_hash: CodeHash<Test>,
_schedule: &Schedule<Test>
_schedule: &Schedule<Test>,
_gas_meter: &mut GasMeter<Test>,
) -> Result<Self, DispatchError> {
Self::from_storage_noinstr(code_hash)
}
@@ -845,11 +888,11 @@ mod tests {
fn drop_from_storage(self) {}
fn add_user(_code_hash: CodeHash<Test>) -> DispatchResult {
Ok(())
fn add_user(_code_hash: CodeHash<Test>) -> Result<u32, DispatchError> {
Ok(0)
}
fn remove_user(_code_hash: CodeHash<Test>) {}
fn remove_user(_code_hash: CodeHash<Test>) -> u32 { 0 }
fn execute<E: Ext<T = Test>>(
self,
@@ -872,6 +915,10 @@ mod tests {
fn occupied_storage(&self) -> u32 {
0
}
fn code_len(&self) -> u32 {
0
}
}
fn exec_success() -> ExecResult {
@@ -954,7 +1001,7 @@ mod tests {
vec![],
).unwrap();
assert!(!output.is_success());
assert!(!output.0.is_success());
assert_eq!(get_balance(&origin), 100);
// the rent is still charged
@@ -1012,8 +1059,8 @@ mod tests {
);
let output = result.unwrap();
assert!(output.is_success());
assert_eq!(output.data, vec![1, 2, 3, 4]);
assert!(output.0.is_success());
assert_eq!(output.0.data, vec![1, 2, 3, 4]);
});
}
@@ -1040,8 +1087,8 @@ mod tests {
);
let output = result.unwrap();
assert!(!output.is_success());
assert_eq!(output.data, vec![1, 2, 3, 4]);
assert!(!output.0.is_success());
assert_eq!(output.0.data, vec![1, 2, 3, 4]);
});
}
@@ -1080,13 +1127,17 @@ mod tests {
let schedule = Contracts::current_schedule();
let subsistence = Contracts::<Test>::subsistence_threshold();
let mut ctx = MockContext::top_level(ALICE, &schedule);
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let executable = MockExecutable::from_storage(
input_data_ch, &schedule, &mut gas_meter
).unwrap();
set_balance(&ALICE, subsistence * 10);
let result = ctx.instantiate(
subsistence * 3,
&mut GasMeter::<Test>::new(GAS_LIMIT),
MockExecutable::from_storage(input_data_ch, &schedule).unwrap(),
&mut gas_meter,
executable,
vec![1, 2, 3, 4],
&[],
);
@@ -1113,7 +1164,7 @@ mod tests {
// Verify that we've got proper error and set `reached_bottom`.
assert_eq!(
r,
Err(Error::<Test>::MaxCallDepthReached.into())
Err((Error::<Test>::MaxCallDepthReached.into(), 0))
);
*reached_bottom = true;
} else {
@@ -1235,12 +1286,16 @@ mod tests {
ExtBuilder::default().existential_deposit(15).build().execute_with(|| {
let schedule = Contracts::current_schedule();
let mut ctx = MockContext::top_level(ALICE, &schedule);
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let executable = MockExecutable::from_storage(
dummy_ch, &schedule, &mut gas_meter
).unwrap();
assert_matches!(
ctx.instantiate(
0, // <- zero endowment
&mut GasMeter::<Test>::new(GAS_LIMIT),
MockExecutable::from_storage(dummy_ch, &schedule).unwrap(),
&mut gas_meter,
executable,
vec![],
&[],
),
@@ -1258,13 +1313,17 @@ mod tests {
ExtBuilder::default().existential_deposit(15).build().execute_with(|| {
let schedule = Contracts::current_schedule();
let mut ctx = MockContext::top_level(ALICE, &schedule);
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let executable = MockExecutable::from_storage(
dummy_ch, &schedule, &mut gas_meter
).unwrap();
set_balance(&ALICE, 1000);
let instantiated_contract_address = assert_matches!(
ctx.instantiate(
100,
&mut GasMeter::<Test>::new(GAS_LIMIT),
MockExecutable::from_storage(dummy_ch, &schedule).unwrap(),
&mut gas_meter,
executable,
vec![],
&[],
),
@@ -1289,13 +1348,17 @@ mod tests {
ExtBuilder::default().existential_deposit(15).build().execute_with(|| {
let schedule = Contracts::current_schedule();
let mut ctx = MockContext::top_level(ALICE, &schedule);
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let executable = MockExecutable::from_storage(
dummy_ch, &schedule, &mut gas_meter
).unwrap();
set_balance(&ALICE, 1000);
let instantiated_contract_address = assert_matches!(
ctx.instantiate(
100,
&mut GasMeter::<Test>::new(GAS_LIMIT),
MockExecutable::from_storage(dummy_ch, &schedule).unwrap(),
&mut gas_meter,
executable,
vec![],
&[],
),
@@ -1317,7 +1380,7 @@ mod tests {
let instantiated_contract_address = Rc::clone(&instantiated_contract_address);
move |ctx| {
// Instantiate a contract and save it's address in `instantiated_contract_address`.
let (address, output) = ctx.ext.instantiate(
let (address, output, _) = ctx.ext.instantiate(
dummy_ch,
Contracts::<Test>::subsistence_threshold() * 3,
ctx.gas_meter,
@@ -1369,10 +1432,10 @@ mod tests {
vec![],
&[],
),
Err(ExecError {
Err((ExecError {
error: DispatchError::Other("It's a trap!"),
origin: ErrorOrigin::Callee,
})
}, 0))
);
exec_success()
@@ -1410,13 +1473,17 @@ mod tests {
.execute_with(|| {
let schedule = Contracts::current_schedule();
let mut ctx = MockContext::top_level(ALICE, &schedule);
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let executable = MockExecutable::from_storage(
terminate_ch, &schedule, &mut gas_meter
).unwrap();
set_balance(&ALICE, 1000);
assert_eq!(
ctx.instantiate(
100,
&mut GasMeter::<Test>::new(GAS_LIMIT),
MockExecutable::from_storage(terminate_ch, &schedule).unwrap(),
&mut gas_meter,
executable,
vec![],
&[],
),
@@ -1445,12 +1512,16 @@ mod tests {
let subsistence = Contracts::<Test>::subsistence_threshold();
let schedule = Contracts::current_schedule();
let mut ctx = MockContext::top_level(ALICE, &schedule);
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
let executable = MockExecutable::from_storage(
rent_allowance_ch, &schedule, &mut gas_meter
).unwrap();
set_balance(&ALICE, subsistence * 10);
let result = ctx.instantiate(
subsistence * 5,
&mut GasMeter::<Test>::new(GAS_LIMIT),
MockExecutable::from_storage(rent_allowance_ch, &schedule).unwrap(),
&mut gas_meter,
executable,
vec![],
&[],
);