[contracts] Implement transparent hashing for contract storage (#11501)

* save

* builds and old tests pass

save:  temporary value dropped while borrowed

save: finally builds

test updated but still fails

* type names enhanced

* VarSizedKey bounded to new Config param

* improved wasm runtime updated funcs

* unstable-interface tests fixed

* benchmarks fixed

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* fixes on feedback

* fixes on feedback applied + make it build

* benchmarks build but fail (old)

* "Original code too large"

* seal_clear_storage bench fixed (code size workaround hack removal tbd)

* bench_seal_clear_storage pass

* bench_seal_take_storage ... ok

* added new seal_set_storage + updated benchmarks

* added new seal_get_storage + updated benchmarks

* added new seal_contains_storage + updated benchmarks

* added tests for _transparent exec functions

* wasm test for clear_storage

* wasm test for take_storage

* wasm test for new set_storage

* wasm test for new get_storage

* wasm test for new contains_storage

* CI fix

* ci fix

* ci fix

* ci fix

* cargo run --quiet --profile=production  --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark pallet --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

* fixes according to the review feedback

* tests & benchmarks fixed

* cargo run --quiet --profile=production  --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark pallet --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

* refactoring

* fix to runtime api

* ci fix

* ctx.get_storage() factored out

* ctx.contains_storage() factored out

* number of batches reduced for transparent hashing storage benchmarks

* contracts RPC & pallet::get_storage to use transparent hashing

* node and rpc updated to use get_storage with VarSizedKey

* refactored (more concize)

* refactored contains_storage (DRYed)

* refactored contains_storage (DRYed)

* fix rpc

* fmt fix

* more fixes in rpc

* rollback `Pallet:get_storage` to Vec<u8> and rpc and node parts related to it

* added `KeyDecodingFailed` error

* Revert weird "fmt fix"

This reverts commit c582cfff4b5cb2c9929fd5e3b45519bb24aeb657.

* node-executor basic test update

* fix node-executor basic test

* benchmarks fix

* more benchmarks fix

* FixedSizedKey is hidden from pub, VarSizedKey is exported as StorageKey

* ci fix

* set_storage benchmark fix

* ci fix

* ci fix

* comments improved

* new error code to rpc: KEY_DECODING_FAILED

* Put `rusty-cachier` before PR merge into `master` for `cargo-check-benches` job

* cargo run --quiet --profile=production  --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark pallet --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

* minor optimization

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: Parity Bot <admin@parity.io>
Co-authored-by: Vladimir Istyufeev <vladimir@parity.io>
Co-authored-by: command-bot <>
This commit is contained in:
Alexander Gryaznov
2022-06-23 15:10:35 +03:00
committed by GitHub
parent 7d2ecc8995
commit 68ea2ab039
13 changed files with 2200 additions and 1158 deletions
+385 -23
View File
@@ -27,12 +27,13 @@ use frame_support::{
storage::{with_transaction, TransactionOutcome},
traits::{Contains, Currency, ExistenceRequirement, OriginTrait, Randomness, Time},
weights::Weight,
Blake2_128Concat, BoundedVec, StorageHasher,
};
use frame_system::RawOrigin;
use pallet_contracts_primitives::ExecReturnValue;
use smallvec::{Array, SmallVec};
use sp_core::{crypto::UncheckedFrom, ecdsa::Public as ECDSAPublic};
use sp_io::crypto::secp256k1_ecdsa_recover_compressed;
use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::blake2_256};
use sp_runtime::traits::Convert;
use sp_std::{marker::PhantomData, mem, prelude::*};
@@ -40,12 +41,40 @@ pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
pub type MomentOf<T> = <<T as Config>::Time as Time>::Moment;
pub type SeedOf<T> = <T as frame_system::Config>::Hash;
pub type BlockNumberOf<T> = <T as frame_system::Config>::BlockNumber;
pub type StorageKey = [u8; 32];
pub type ExecResult = Result<ExecReturnValue, ExecError>;
/// A type that represents a topic of an event. At the moment a hash is used.
pub type TopicOf<T> = <T as frame_system::Config>::Hash;
/// Type for fix sized storage key.
pub type FixSizedKey = [u8; 32];
/// Type for variable sized storage key. Used for transparent hashing.
pub type VarSizedKey<T> = BoundedVec<u8, <T as Config>::MaxStorageKeyLen>;
/// Trait for hashing storage keys.
pub trait StorageKey<T>
where
T: Config,
{
fn hash(&self) -> Vec<u8>;
}
impl<T: Config> StorageKey<T> for FixSizedKey {
fn hash(&self) -> Vec<u8> {
blake2_256(self.as_slice()).to_vec()
}
}
impl<T> StorageKey<T> for VarSizedKey<T>
where
T: Config,
{
fn hash(&self) -> Vec<u8> {
Blake2_128Concat::hash(self.as_slice())
}
}
/// Origin of the error.
///
/// Call or instantiate both called into other contracts and pass through errors happening
@@ -140,19 +169,44 @@ pub trait Ext: sealing::Sealed {
///
/// Returns `None` if the `key` wasn't previously set by `set_storage` or
/// was deleted.
fn get_storage(&mut self, key: &StorageKey) -> Option<Vec<u8>>;
fn get_storage(&mut self, key: &FixSizedKey) -> Option<Vec<u8>>;
/// This is a variation of `get_storage()` to be used with transparent hashing.
/// These two will be merged into a single function after some refactoring is done.
/// Returns the storage entry of the executing account by the given `key`.
///
/// Returns `None` if the `key` wasn't previously set by `set_storage` or
/// was deleted.
fn get_storage_transparent(&mut self, key: &VarSizedKey<Self::T>) -> Option<Vec<u8>>;
/// Returns `Some(len)` (in bytes) if a storage item exists at `key`.
///
/// Returns `None` if the `key` wasn't previously set by `set_storage` or
/// was deleted.
fn get_storage_size(&mut self, key: &StorageKey) -> Option<u32>;
fn get_storage_size(&mut self, key: &FixSizedKey) -> Option<u32>;
/// This is the variation of `get_storage_size()` to be used with transparent hashing.
/// These two will be merged into a single function after some refactoring is done.
/// Returns `Some(len)` (in bytes) if a storage item exists at `key`.
///
/// Returns `None` if the `key` wasn't previously set by `set_storage` or
/// was deleted.
fn get_storage_size_transparent(&mut self, key: &VarSizedKey<Self::T>) -> Option<u32>;
/// Sets the storage entry by the given key to the specified value. If `value` is `None` then
/// the storage entry is deleted.
fn set_storage(
&mut self,
key: StorageKey,
key: &FixSizedKey,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError>;
/// This is the variation of `set_storage()` to be used with transparent hashing.
/// These two will be merged into a single function after some refactoring is done.
fn set_storage_transparent(
&mut self,
key: &VarSizedKey<Self::T>,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError>;
@@ -1085,24 +1139,48 @@ where
Self::transfer(ExistenceRequirement::KeepAlive, &self.top_frame().account_id, to, value)
}
fn get_storage(&mut self, key: &StorageKey) -> Option<Vec<u8>> {
fn get_storage(&mut self, key: &FixSizedKey) -> Option<Vec<u8>> {
Storage::<T>::read(&self.top_frame_mut().contract_info().trie_id, key)
}
fn get_storage_size(&mut self, key: &StorageKey) -> Option<u32> {
fn get_storage_transparent(&mut self, key: &VarSizedKey<T>) -> Option<Vec<u8>> {
Storage::<T>::read(&self.top_frame_mut().contract_info().trie_id, key)
}
fn get_storage_size(&mut self, key: &FixSizedKey) -> Option<u32> {
Storage::<T>::size(&self.top_frame_mut().contract_info().trie_id, key)
}
fn get_storage_size_transparent(&mut self, key: &VarSizedKey<T>) -> Option<u32> {
Storage::<T>::size(&self.top_frame_mut().contract_info().trie_id, key)
}
fn set_storage(
&mut self,
key: StorageKey,
key: &FixSizedKey,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError> {
let frame = self.top_frame_mut();
Storage::<T>::write(
&frame.contract_info.get(&frame.account_id).trie_id,
&key,
key,
value,
Some(&mut frame.nested_storage),
take_old,
)
}
fn set_storage_transparent(
&mut self,
key: &VarSizedKey<T>,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError> {
let frame = self.top_frame_mut();
Storage::<T>::write(
&frame.contract_info.get(&frame.account_id).trie_id,
key,
value,
Some(&mut frame.nested_storage),
take_old,
@@ -2635,35 +2713,35 @@ mod tests {
let code_hash = MockLoader::insert(Call, |ctx, _| {
// Write
assert_eq!(
ctx.ext.set_storage([1; 32], Some(vec![1, 2, 3]), false),
ctx.ext.set_storage(&[1; 32], Some(vec![1, 2, 3]), false),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage([2; 32], Some(vec![4, 5, 6]), true),
ctx.ext.set_storage(&[2; 32], Some(vec![4, 5, 6]), true),
Ok(WriteOutcome::New)
);
assert_eq!(ctx.ext.set_storage([3; 32], None, false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage([4; 32], None, true), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage([5; 32], Some(vec![]), false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage([6; 32], Some(vec![]), true), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[3; 32], None, false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[4; 32], None, true), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[5; 32], Some(vec![]), false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[6; 32], Some(vec![]), true), Ok(WriteOutcome::New));
// Overwrite
assert_eq!(
ctx.ext.set_storage([1; 32], Some(vec![42]), false),
ctx.ext.set_storage(&[1; 32], Some(vec![42]), false),
Ok(WriteOutcome::Overwritten(3))
);
assert_eq!(
ctx.ext.set_storage([2; 32], Some(vec![48]), true),
ctx.ext.set_storage(&[2; 32], Some(vec![48]), true),
Ok(WriteOutcome::Taken(vec![4, 5, 6]))
);
assert_eq!(ctx.ext.set_storage([3; 32], None, false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage([4; 32], None, true), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[3; 32], None, false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[4; 32], None, true), Ok(WriteOutcome::New));
assert_eq!(
ctx.ext.set_storage([5; 32], Some(vec![]), false),
ctx.ext.set_storage(&[5; 32], Some(vec![]), false),
Ok(WriteOutcome::Overwritten(0))
);
assert_eq!(
ctx.ext.set_storage([6; 32], Some(vec![]), true),
ctx.ext.set_storage(&[6; 32], Some(vec![]), true),
Ok(WriteOutcome::Taken(vec![]))
);
@@ -2690,14 +2768,175 @@ mod tests {
});
}
#[test]
fn set_storage_transparent_works() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
// Write
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([1; 64].to_vec()).unwrap(),
Some(vec![1, 2, 3]),
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([2; 19].to_vec()).unwrap(),
Some(vec![4, 5, 6]),
true
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([3; 19].to_vec()).unwrap(),
None,
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([4; 64].to_vec()).unwrap(),
None,
true
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([5; 30].to_vec()).unwrap(),
Some(vec![]),
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([6; 128].to_vec()).unwrap(),
Some(vec![]),
true
),
Ok(WriteOutcome::New)
);
// Overwrite
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([1; 64].to_vec()).unwrap(),
Some(vec![42, 43, 44]),
false
),
Ok(WriteOutcome::Overwritten(3))
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([2; 19].to_vec()).unwrap(),
Some(vec![48]),
true
),
Ok(WriteOutcome::Taken(vec![4, 5, 6]))
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([3; 19].to_vec()).unwrap(),
None,
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([4; 64].to_vec()).unwrap(),
None,
true
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([5; 30].to_vec()).unwrap(),
Some(vec![]),
false
),
Ok(WriteOutcome::Overwritten(0))
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([6; 128].to_vec()).unwrap(),
Some(vec![]),
true
),
Ok(WriteOutcome::Taken(vec![]))
);
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let min_balance = <Test as Config>::Currency::minimum_balance();
let schedule = <Test as Config>::Schedule::get();
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
set_balance(&ALICE, min_balance * 1000);
place_contract(&BOB, code_hash);
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, 0).unwrap();
assert_ok!(MockStack::run_call(
ALICE,
BOB,
&mut gas_meter,
&mut storage_meter,
&schedule,
0,
vec![],
None,
));
});
}
#[test]
fn get_storage_works() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
assert_eq!(
ctx.ext.set_storage(&[1; 32], Some(vec![1, 2, 3]), false),
Ok(WriteOutcome::New)
);
assert_eq!(ctx.ext.set_storage(&[2; 32], Some(vec![]), false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.get_storage(&[1; 32]), Some(vec![1, 2, 3]));
assert_eq!(ctx.ext.get_storage(&[2; 32]), Some(vec![]));
assert_eq!(ctx.ext.get_storage(&[3; 32]), None);
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let min_balance = <Test as Config>::Currency::minimum_balance();
let schedule = <Test as Config>::Schedule::get();
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
set_balance(&ALICE, min_balance * 1000);
place_contract(&BOB, code_hash);
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, 0).unwrap();
assert_ok!(MockStack::run_call(
ALICE,
BOB,
&mut gas_meter,
&mut storage_meter,
&schedule,
0,
vec![],
None,
));
});
}
#[test]
fn get_storage_size_works() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
assert_eq!(
ctx.ext.set_storage([1; 32], Some(vec![1, 2, 3]), false),
ctx.ext.set_storage(&[1; 32], Some(vec![1, 2, 3]), false),
Ok(WriteOutcome::New)
);
assert_eq!(ctx.ext.set_storage([2; 32], Some(vec![]), false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.set_storage(&[2; 32], Some(vec![]), false), Ok(WriteOutcome::New));
assert_eq!(ctx.ext.get_storage_size(&[1; 32]), Some(3));
assert_eq!(ctx.ext.get_storage_size(&[2; 32]), Some(0));
assert_eq!(ctx.ext.get_storage_size(&[3; 32]), None);
@@ -2724,6 +2963,129 @@ mod tests {
));
});
}
#[test]
fn get_storage_transparent_works() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([1; 19].to_vec()).unwrap(),
Some(vec![1, 2, 3]),
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([2; 16].to_vec()).unwrap(),
Some(vec![]),
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.get_storage_transparent(
&VarSizedKey::<Test>::try_from([1; 19].to_vec()).unwrap()
),
Some(vec![1, 2, 3])
);
assert_eq!(
ctx.ext.get_storage_transparent(
&VarSizedKey::<Test>::try_from([2; 16].to_vec()).unwrap()
),
Some(vec![])
);
assert_eq!(
ctx.ext.get_storage_transparent(
&VarSizedKey::<Test>::try_from([3; 8].to_vec()).unwrap()
),
None
);
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let min_balance = <Test as Config>::Currency::minimum_balance();
let schedule = <Test as Config>::Schedule::get();
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
set_balance(&ALICE, min_balance * 1000);
place_contract(&BOB, code_hash);
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, 0).unwrap();
assert_ok!(MockStack::run_call(
ALICE,
BOB,
&mut gas_meter,
&mut storage_meter,
&schedule,
0,
vec![],
None,
));
});
}
#[test]
fn get_storage_size_transparent_works() {
let code_hash = MockLoader::insert(Call, |ctx, _| {
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([1; 19].to_vec()).unwrap(),
Some(vec![1, 2, 3]),
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.set_storage_transparent(
&VarSizedKey::<Test>::try_from([2; 16].to_vec()).unwrap(),
Some(vec![]),
false
),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.get_storage_size_transparent(
&VarSizedKey::<Test>::try_from([1; 19].to_vec()).unwrap()
),
Some(3)
);
assert_eq!(
ctx.ext.get_storage_size_transparent(
&VarSizedKey::<Test>::try_from([2; 16].to_vec()).unwrap()
),
Some(0)
);
assert_eq!(
ctx.ext.get_storage_size_transparent(
&VarSizedKey::<Test>::try_from([3; 8].to_vec()).unwrap()
),
None
);
exec_success()
});
ExtBuilder::default().build().execute_with(|| {
let min_balance = <Test as Config>::Currency::minimum_balance();
let schedule = <Test as Config>::Schedule::get();
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
set_balance(&ALICE, min_balance * 1000);
place_contract(&BOB, code_hash);
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, 0).unwrap();
assert_ok!(MockStack::run_call(
ALICE,
BOB,
&mut gas_meter,
&mut storage_meter,
&schedule,
0,
vec![],
None,
));
});
}
#[test]
fn ecdsa_to_eth_address_returns_proper_value() {
let bob_ch = MockLoader::insert(Call, |ctx, _| {