-
Notifications
You must be signed in to change notification settings - Fork 680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[pallet_contracts] Modify the storage host function benchmarks to run on an unbalanced storage trie. #5036
[pallet_contracts] Modify the storage host function benchmarks to run on an unbalanced storage trie. #5036
Changes from all commits
749ad4a
dcaaebf
912ac16
112f13b
d67ba3b
afe96f2
74aaeca
b001a79
97ab9eb
c1d7879
7f378ae
2259be8
b78dc7b
82a92aa
eba012b
8df669e
e45bc36
7e53bc2
8941994
ef4830d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 | ||
# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json | ||
|
||
title: "[pallet_contracts] Modify the storage host function benchmarks to be run on an unbalanced storage trie" | ||
|
||
doc: | ||
- audience: Runtime User | ||
description: | | ||
This PR modifies the storage host function benchmarks. Previously, they were run | ||
on an empty storage trie. Now, they are run on an unbalanced storage trie | ||
to reflect the worst-case scenario. This approach increases the storage host | ||
function weights and decreases the probability of DoS attacks. | ||
|
||
crates: | ||
- name: pallet-contracts | ||
bump: patch |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,6 +41,7 @@ use frame_benchmarking::v2::*; | |
use frame_support::{ | ||
self, assert_ok, | ||
pallet_prelude::StorageVersion, | ||
storage::child, | ||
traits::{fungible::InspectHold, Currency}, | ||
weights::{Weight, WeightMeter}, | ||
}; | ||
|
@@ -63,6 +64,9 @@ const API_BENCHMARK_RUNS: u32 = 1600; | |
/// benchmarks are faster. | ||
const INSTR_BENCHMARK_RUNS: u32 = 5000; | ||
|
||
/// Number of layers in a Radix16 unbalanced trie. | ||
const UNBALANCED_TRIE_LAYERS: u32 = 20; | ||
|
||
/// An instantiated and deployed contract. | ||
#[derive(Clone)] | ||
struct Contract<T: Config> { | ||
|
@@ -152,6 +156,36 @@ where | |
Ok(()) | ||
} | ||
|
||
/// Create a new contract with the specified unbalanced storage trie. | ||
fn with_unbalanced_storage_trie(code: WasmModule<T>, key: &[u8]) -> Result<Self, &'static str> { | ||
if (key.len() as u32) < (UNBALANCED_TRIE_LAYERS + 1) / 2 { | ||
return Err("Key size too small to create the specified trie"); | ||
} | ||
|
||
let value = vec![16u8; T::Schedule::get().limits.payload_len as usize]; | ||
let contract = Contract::<T>::new(code, vec![])?; | ||
let info = contract.info()?; | ||
let child_trie_info = info.child_trie_info(); | ||
child::put_raw(&child_trie_info, &key, &value); | ||
for l in 0..UNBALANCED_TRIE_LAYERS { | ||
let pos = l as usize / 2; | ||
let mut key_new = key.to_vec(); | ||
for i in 0u8..16 { | ||
key_new[pos] = if l % 2 == 0 { | ||
(key_new[pos] & 0xF0) | i | ||
} else { | ||
(key_new[pos] & 0x0F) | (i << 4) | ||
}; | ||
|
||
if key == &key_new { | ||
continue | ||
} | ||
child::put_raw(&child_trie_info, &key_new, &value); | ||
} | ||
} | ||
Ok(contract) | ||
Comment on lines
+169
to
+186
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is quite hard to scan, might be nice to add some explanation on how you construct the tree |
||
} | ||
|
||
/// Get the `ContractInfo` of the `addr` or an error if it no longer exists. | ||
fn address_info(addr: &T::AccountId) -> Result<ContractInfo<T>, &'static str> { | ||
ContractInfoOf::<T>::get(addr).ok_or("Expected contract to exist at this point.") | ||
|
@@ -1014,6 +1048,102 @@ mod benchmarks { | |
assert_eq!(setup.debug_message().unwrap().len() as u32, i); | ||
} | ||
|
||
#[benchmark(skip_meta, pov_mode = Measured)] | ||
fn get_storage_empty() -> Result<(), BenchmarkError> { | ||
let max_key_len = T::MaxStorageKeyLen::get(); | ||
let key = vec![0u8; max_key_len as usize]; | ||
let max_value_len = T::Schedule::get().limits.payload_len as usize; | ||
let value = vec![1u8; max_value_len]; | ||
|
||
let instance = Contract::<T>::new(WasmModule::dummy(), vec![])?; | ||
let info = instance.info()?; | ||
let child_trie_info = info.child_trie_info(); | ||
info.bench_write_raw(&key, Some(value.clone()), false) | ||
.map_err(|_| "Failed to write to storage during setup.")?; | ||
|
||
let result; | ||
#[block] | ||
{ | ||
result = child::get_raw(&child_trie_info, &key); | ||
} | ||
|
||
assert_eq!(result, Some(value)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we somehow verify that we did hit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that we are providing a key as a parameter, we can be sure that we will hit it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean is there a way to assert that hitting this storage is traversing 20 nodes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are constructing the trie around the provided key. Assuming the trie construction is done correctly, we should hit 20 layers by writing to this key. I am not sure if we can assert this somehow. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. right looks like it can't be done, the weights results make sense, and I just doubled check by writing the proof to disk that the storage_full has indeed 20 keys more |
||
Ok(()) | ||
} | ||
|
||
#[benchmark(skip_meta, pov_mode = Measured)] | ||
fn get_storage_full() -> Result<(), BenchmarkError> { | ||
let max_key_len = T::MaxStorageKeyLen::get(); | ||
let key = vec![0u8; max_key_len as usize]; | ||
let max_value_len = T::Schedule::get().limits.payload_len; | ||
let value = vec![1u8; max_value_len as usize]; | ||
|
||
let instance = Contract::<T>::with_unbalanced_storage_trie(WasmModule::dummy(), &key)?; | ||
let info = instance.info()?; | ||
let child_trie_info = info.child_trie_info(); | ||
info.bench_write_raw(&key, Some(value.clone()), false) | ||
.map_err(|_| "Failed to write to storage during setup.")?; | ||
|
||
let result; | ||
#[block] | ||
{ | ||
result = child::get_raw(&child_trie_info, &key); | ||
} | ||
|
||
assert_eq!(result, Some(value)); | ||
Ok(()) | ||
} | ||
|
||
#[benchmark(skip_meta, pov_mode = Measured)] | ||
fn set_storage_empty() -> Result<(), BenchmarkError> { | ||
let max_key_len = T::MaxStorageKeyLen::get(); | ||
let key = vec![0u8; max_key_len as usize]; | ||
let max_value_len = T::Schedule::get().limits.payload_len as usize; | ||
let value = vec![1u8; max_value_len]; | ||
|
||
let instance = Contract::<T>::new(WasmModule::dummy(), vec![])?; | ||
let info = instance.info()?; | ||
let child_trie_info = info.child_trie_info(); | ||
info.bench_write_raw(&key, Some(vec![42u8; max_value_len]), false) | ||
.map_err(|_| "Failed to write to storage during setup.")?; | ||
|
||
let val = Some(value.clone()); | ||
let result; | ||
#[block] | ||
{ | ||
result = info.bench_write_raw(&key, val, true); | ||
} | ||
|
||
assert_ok!(result); | ||
assert_eq!(child::get_raw(&child_trie_info, &key).unwrap(), value); | ||
Ok(()) | ||
} | ||
|
||
#[benchmark(skip_meta, pov_mode = Measured)] | ||
fn set_storage_full() -> Result<(), BenchmarkError> { | ||
let max_key_len = T::MaxStorageKeyLen::get(); | ||
let key = vec![0u8; max_key_len as usize]; | ||
let max_value_len = T::Schedule::get().limits.payload_len; | ||
let value = vec![1u8; max_value_len as usize]; | ||
|
||
let instance = Contract::<T>::with_unbalanced_storage_trie(WasmModule::dummy(), &key)?; | ||
let info = instance.info()?; | ||
let child_trie_info = info.child_trie_info(); | ||
info.bench_write_raw(&key, Some(vec![42u8; max_value_len as usize]), false) | ||
.map_err(|_| "Failed to write to storage during setup.")?; | ||
|
||
let val = Some(value.clone()); | ||
let result; | ||
#[block] | ||
{ | ||
result = info.bench_write_raw(&key, val, true); | ||
} | ||
|
||
assert_ok!(result); | ||
assert_eq!(child::get_raw(&child_trie_info, &key).unwrap(), value); | ||
Ok(()) | ||
} | ||
|
||
// n: new byte size | ||
// o: old byte size | ||
#[benchmark(skip_meta, pov_mode = Measured)] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -164,13 +164,35 @@ impl<T: Config> ContractInfo<T> { | |
storage_meter: Option<&mut meter::NestedMeter<T>>, | ||
take: bool, | ||
) -> Result<WriteOutcome, DispatchError> { | ||
let child_trie_info = &self.child_trie_info(); | ||
let hashed_key = key.hash(); | ||
self.write_raw(&hashed_key, new_value, storage_meter, take) | ||
} | ||
|
||
/// Update a storage entry into a contract's kv storage. | ||
/// Function used in benchmarks, which can simulate prefix collision in keys. | ||
#[cfg(feature = "runtime-benchmarks")] | ||
pub fn bench_write_raw( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of this call we could add and use Key::Raw variant, but I am not sure if it is better option, WDYT? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wouldn't call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suggest keeping it this way. We can't use |
||
&self, | ||
key: &[u8], | ||
new_value: Option<Vec<u8>>, | ||
take: bool, | ||
) -> Result<WriteOutcome, DispatchError> { | ||
self.write_raw(key, new_value, None, take) | ||
} | ||
|
||
fn write_raw( | ||
&self, | ||
key: &[u8], | ||
new_value: Option<Vec<u8>>, | ||
storage_meter: Option<&mut meter::NestedMeter<T>>, | ||
take: bool, | ||
) -> Result<WriteOutcome, DispatchError> { | ||
let child_trie_info = &self.child_trie_info(); | ||
let (old_len, old_value) = if take { | ||
let val = child::get_raw(child_trie_info, &hashed_key); | ||
let val = child::get_raw(child_trie_info, key); | ||
(val.as_ref().map(|v| v.len() as u32), val) | ||
} else { | ||
(child::len(child_trie_info, &hashed_key), None) | ||
(child::len(child_trie_info, key), None) | ||
}; | ||
|
||
if let Some(storage_meter) = storage_meter { | ||
|
@@ -196,8 +218,8 @@ impl<T: Config> ContractInfo<T> { | |
} | ||
|
||
match &new_value { | ||
Some(new_value) => child::put_raw(child_trie_info, &hashed_key, new_value), | ||
None => child::kill(child_trie_info, &hashed_key), | ||
Some(new_value) => child::put_raw(child_trie_info, key, new_value), | ||
None => child::kill(child_trie_info, key), | ||
} | ||
|
||
Ok(match (old_len, old_value) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Contracts can write in storage in a way that produces an unbalanced trie? I expected this to be impossible/prevented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All the keys for stored values are hashed using Blake2, so it is unlikely that this will happen in normal use. Main issue here is that we used empty trie for benchmarks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A contract can't create unbalanced tries because we hash the key from the contract. However, a contract could have a balanced but very big trie. The unbalanced trie is just use to simulate a deep trie for benchmarking.