Skip to content
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] Add support for transient storage in contracts host functions #4566

Merged
merged 70 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
1732628
Add transient storage
smiasojed May 23, 2024
6cda150
Add unit tests
smiasojed May 23, 2024
6670170
Merge remote-tracking branch 'origin/master' into sm/contracts-tstore
smiasojed May 23, 2024
826e073
Add storage limit
smiasojed May 23, 2024
daf9ad1
Limit storage per contract
smiasojed May 24, 2024
d4a29be
Refactored
smiasojed May 28, 2024
53ec270
Add terminate function to transient storage
smiasojed May 30, 2024
33f6c2d
Add unit tests
smiasojed May 31, 2024
199edaa
Refactored to single BtreeMap
smiasojed Jun 3, 2024
e683b72
Add comments
smiasojed Jun 3, 2024
1c3c844
Cleanup
smiasojed Jun 4, 2024
18cd9ad
Fix unit tests
smiasojed Jun 4, 2024
d5736d5
Merge remote-tracking branch 'origin/master' into sm/contracts-tstore
smiasojed Jun 4, 2024
58d87dc
Fix tests after master merge
smiasojed Jun 4, 2024
29f1305
Rename storage err
smiasojed Jun 5, 2024
63aaaaf
Benchmarking test
smiasojed Jun 18, 2024
78960e0
Fix unit tests
smiasojed Jun 18, 2024
f09b80b
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jun 18, 2024
6d63e58
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jun 18, 2024
6f713ae
Max storage elements set to 2000
smiasojed Jun 18, 2024
d6e1c43
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jun 18, 2024
8c11871
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jun 18, 2024
664221b
Storage len 50
smiasojed Jun 18, 2024
d579005
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jun 18, 2024
6667429
Merge remote-tracking branch 'origin/master' into sm/contracts-tstore
smiasojed Jun 20, 2024
8d91109
Merge remote-tracking branch 'origin/master' into sm/contracts-tstore
smiasojed Jun 24, 2024
5794910
Change transient storage meter
smiasojed Jun 25, 2024
da8316e
Fix benchmark warnings
smiasojed Jun 25, 2024
ac65687
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jun 25, 2024
d8298f0
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jun 25, 2024
339e935
Refactor benchmarks
smiasojed Jun 26, 2024
735faae
Refactor transient storage
smiasojed Jun 26, 2024
51fb39c
Add test benchmarks
smiasojed Jun 27, 2024
2f3abc4
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jun 27, 2024
b2c7005
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jun 27, 2024
01365cf
Refactored benchmarks
smiasojed Jun 27, 2024
a2dfb23
Add wasm tests
smiasojed Jun 28, 2024
ca1a323
Add E2E tests
smiasojed Jun 28, 2024
399d541
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jun 28, 2024
6d8dc05
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jun 28, 2024
1ed42f4
Add docs
smiasojed Jul 1, 2024
3ec4626
Fix tests
smiasojed Jul 1, 2024
2d3a823
Fix contracts-rococo configuration
smiasojed Jul 1, 2024
09ff908
Fmt
smiasojed Jul 1, 2024
5fa1b1b
Update PR doc
smiasojed Jul 1, 2024
f6aeb87
Cleanup
smiasojed Jul 1, 2024
4863431
Remove transient storage limit in benchmark
smiasojed Jul 2, 2024
baa943f
Cleanup
smiasojed Jul 2, 2024
7257857
Merge remote-tracking branch 'origin/master' into sm/contracts-tstore
smiasojed Jul 2, 2024
bcde2de
Fix description
smiasojed Jul 2, 2024
9a204bd
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jul 2, 2024
037f0e6
Update substrate/frame/contracts/src/exec.rs
smiasojed Jul 3, 2024
ea62ece
Update substrate/frame/contracts/src/exec.rs
smiasojed Jul 3, 2024
0888164
Update substrate/frame/contracts/src/transient_storage.rs
smiasojed Jul 4, 2024
176bbc5
Update substrate/frame/contracts/src/transient_storage.rs
smiasojed Jul 4, 2024
fb8f0ad
Change fn visibility and code cleanup
smiasojed Jul 4, 2024
4b148b9
Update tests
smiasojed Jul 4, 2024
057fd0d
Update tests
smiasojed Jul 4, 2024
6acc50f
Remove double key creation
smiasojed Jul 5, 2024
b9aa452
Update panic messages
smiasojed Jul 5, 2024
2bb933e
Update substrate/frame/contracts/src/lib.rs
smiasojed Jul 5, 2024
0c189a6
Cleanup benchmarks
smiasojed Jul 5, 2024
865886d
Improve panic messages
smiasojed Jul 5, 2024
03f10f9
Marked functions as unstable
smiasojed Jul 8, 2024
5c59336
Sealed HostFn trait
smiasojed Jul 9, 2024
8d36584
Refactor benchmarks
smiasojed Jul 9, 2024
4db9c95
Merge branch 'master' of https://github.com/paritytech/polkadot-sdk i…
Jul 9, 2024
f2cfb76
".git/.scripts/commands/bench/bench.sh" --subcommand=pallet --runtime…
Jul 9, 2024
3a8c2d2
Merge remote-tracking branch 'origin/master' into sm/contracts-tstore
smiasojed Jul 10, 2024
45603b8
Comment added
smiasojed Jul 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,7 @@ impl pallet_contracts::Config for Runtime {
type UploadOrigin = EnsureSigned<Self::AccountId>;
type InstantiateOrigin = EnsureSigned<Self::AccountId>;
type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>;
type MaxTransientStorageLen = ConstU32<{ 1 * 1024 * 1024 }>;
type RuntimeHoldReason = RuntimeHoldReason;
#[cfg(not(feature = "runtime-benchmarks"))]
type Migrations = ();
Expand Down
201 changes: 198 additions & 3 deletions substrate/frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
gas::GasMeter,
primitives::{ExecReturnValue, StorageDeposit},
storage::{self, meter::Diff, WriteOutcome},
transient_storage::TransientStorage,
BalanceOf, CodeHash, CodeInfo, CodeInfoOf, Config, ContractInfo, ContractInfoOf,
DebugBufferVec, Determinism, Error, Event, Nonce, Origin, Pallet as Contracts, Schedule,
LOG_TARGET,
Expand Down Expand Up @@ -209,6 +210,27 @@ pub trait Ext: sealing::Sealed {
take_old: bool,
) -> Result<WriteOutcome, DispatchError>;

/// 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_transient_storage(&self, key: &Key<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_transient_storage_size(&self, key: &Key<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_transient_storage(
&mut self,
key: &Key<Self::T>,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError>;

/// Returns the caller.
fn caller(&self) -> Origin<Self::T>;

Expand Down Expand Up @@ -473,6 +495,8 @@ pub struct Stack<'a, T: Config, E> {
debug_message: Option<&'a mut DebugBufferVec<T>>,
/// The determinism requirement of this call stack.
determinism: Determinism,
/// Transient storage
transient_storage: TransientStorage<T>,
/// No executable is held by the struct but influences its behaviour.
_phantom: PhantomData<E>,
}
Expand Down Expand Up @@ -784,6 +808,7 @@ where
false,
)?;

let transient_storage_limit = T::MaxTransientStorageLen::get();
let stack = Self {
origin,
schedule,
Expand All @@ -796,6 +821,11 @@ where
frames: Default::default(),
debug_message,
determinism,
transient_storage: TransientStorage::new(
transient_storage_limit,
transient_storage_limit
.saturating_div((T::CallStack::size() as u32).saturating_add(1)),
),
_phantom: Default::default(),
};

Expand Down Expand Up @@ -926,6 +956,9 @@ where
let entry_point = frame.entry_point;
let delegated_code_hash =
if frame.delegate_caller.is_some() { Some(*executable.code_hash()) } else { None };

self.transient_storage.start_transaction();

let do_transaction = || {
// We need to charge the storage deposit before the initial transfer so that
// it can create the account in case the initial transfer is < ed.
Expand Down Expand Up @@ -1046,6 +1079,12 @@ where
Err(error) => (false, Err(error.into())),
};

if success {
self.transient_storage.commit_transaction();
} else {
self.transient_storage.rollback_transaction();
}

athei marked this conversation as resolved.
Show resolved Hide resolved
self.pop_frame(success);
output
}
Expand Down Expand Up @@ -1326,22 +1365,24 @@ where
return Err(Error::<T>::TerminatedWhileReentrant.into())
}
let frame = self.top_frame_mut();
let account_id = frame.account_id.clone();
let info = frame.terminate();
frame.nested_storage.terminate(&info, beneficiary.clone());

info.queue_trie_for_deletion();
ContractInfoOf::<T>::remove(&frame.account_id);
ContractInfoOf::<T>::remove(&account_id);
Self::decrement_refcount(info.code_hash);

for (code_hash, deposit) in info.delegate_dependencies() {
Self::decrement_refcount(*code_hash);
frame
.nested_storage
.charge_deposit(frame.account_id.clone(), StorageDeposit::Refund(*deposit));
.charge_deposit(account_id.clone(), StorageDeposit::Refund(*deposit));
}

self.transient_storage.remove(&account_id);
Contracts::<T>::deposit_event(Event::Terminated {
contract: frame.account_id.clone(),
contract: account_id,
beneficiary: beneficiary.clone(),
});
Ok(())
Expand Down Expand Up @@ -1374,6 +1415,24 @@ where
)
}

fn get_transient_storage(&self, key: &Key<T>) -> Option<Vec<u8>> {
self.transient_storage.read(self.address(), key)
}

fn get_transient_storage_size(&self, key: &Key<T>) -> Option<u32> {
self.transient_storage.read(self.address(), key).map(|value| value.len() as _)
}

fn set_transient_storage(
&mut self,
key: &Key<T>,
value: Option<Vec<u8>>,
take_old: bool,
) -> Result<WriteOutcome, DispatchError> {
let account_id = self.address().clone();
self.transient_storage.write(&account_id, key, value, take_old)
}

fn address(&self) -> &T::AccountId {
&self.top_frame().account_id
}
Expand Down Expand Up @@ -3826,6 +3885,142 @@ mod tests {
});
}

#[test]
fn transient_storage_works() {
// Call stack: BOB -> CHARLIE(success) -> BOB' (success)
let storage_key_1 = &Key::Fix([1; 32]);
let storage_key_2 = &Key::Fix([2; 32]);
let code_bob = MockLoader::insert(Call, |ctx, _| {
if ctx.input_data[0] == 0 {
assert_eq!(
ctx.ext.set_transient_storage(storage_key_1, Some(vec![1, 2]), false),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.call(
Weight::zero(),
BalanceOf::<Test>::zero(),
CHARLIE,
0,
vec![],
true,
false,
),
exec_success()
);
assert_eq!(ctx.ext.get_transient_storage(storage_key_1), Some(vec![3]));
assert_eq!(ctx.ext.get_transient_storage(storage_key_2), Some(vec![4]));
} else {
assert_eq!(
ctx.ext.set_transient_storage(storage_key_1, Some(vec![3]), true),
Ok(WriteOutcome::Taken(vec![1, 2]))
);
assert_eq!(
ctx.ext.set_transient_storage(storage_key_2, Some(vec![4]), false),
Ok(WriteOutcome::New)
);
}
exec_success()
});
let code_charlie = MockLoader::insert(Call, |ctx, _| {
assert!(ctx
.ext
.call(Weight::zero(), BalanceOf::<Test>::zero(), BOB, 0, vec![99], true, false)
.is_ok());
// CHARLIE can not read BOB`s storage.
assert_eq!(ctx.ext.get_transient_storage(storage_key_1), None);
exec_success()
});

// This one tests passing the input data into a contract via call.
ExtBuilder::default().build().execute_with(|| {
let schedule = <Test as Config>::Schedule::get();
place_contract(&BOB, code_bob);
place_contract(&CHARLIE, code_charlie);
let contract_origin = Origin::from_account_id(ALICE);
let mut storage_meter =
storage::meter::Meter::new(&contract_origin, Some(0), 0).unwrap();

let result = MockStack::run_call(
contract_origin,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&mut storage_meter,
&schedule,
0,
vec![0],
None,
Determinism::Enforced,
);
assert_matches!(result, Ok(_));
});
}

#[test]
fn transient_storage_rollback_works() {
// Call stack: BOB -> CHARLIE (trap) -> BOB' (success)
let storage_key = &Key::Fix([1; 32]);
let code_bob = MockLoader::insert(Call, |ctx, _| {
if ctx.input_data[0] == 0 {
assert_eq!(
ctx.ext.set_transient_storage(storage_key, Some(vec![1, 2]), false),
Ok(WriteOutcome::New)
);
assert_eq!(
ctx.ext.call(
Weight::zero(),
BalanceOf::<Test>::zero(),
CHARLIE,
0,
vec![],
true,
false
),
exec_trapped()
);
assert_eq!(ctx.ext.get_transient_storage(storage_key), Some(vec![1, 2]));
} else {
let overwritten_length = ctx.ext.get_transient_storage_size(storage_key).unwrap();
assert_eq!(
ctx.ext.set_transient_storage(storage_key, Some(vec![3]), false),
Ok(WriteOutcome::Overwritten(overwritten_length))
);
assert_eq!(ctx.ext.get_transient_storage(storage_key), Some(vec![3]));
}
exec_success()
});
let code_charlie = MockLoader::insert(Call, |ctx, _| {
assert!(ctx
.ext
.call(Weight::zero(), BalanceOf::<Test>::zero(), BOB, 0, vec![99], true, false)
.is_ok());
exec_trapped()
});

// This one tests passing the input data into a contract via call.
ExtBuilder::default().build().execute_with(|| {
let schedule = <Test as Config>::Schedule::get();
place_contract(&BOB, code_bob);
place_contract(&CHARLIE, code_charlie);
let contract_origin = Origin::from_account_id(ALICE);
let mut storage_meter =
storage::meter::Meter::new(&contract_origin, Some(0), 0).unwrap();

let result = MockStack::run_call(
contract_origin,
BOB,
&mut GasMeter::<Test>::new(GAS_LIMIT),
&mut storage_meter,
&schedule,
0,
vec![0],
None,
Determinism::Enforced,
);
assert_matches!(result, Ok(_));
});
}

#[test]
fn ecdsa_to_eth_address_returns_proper_value() {
let bob_ch = MockLoader::insert(Call, |ctx, _| {
Expand Down
23 changes: 19 additions & 4 deletions substrate/frame/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub use primitives::*;

mod schedule;
mod storage;
mod transient_storage;
mod wasm;

pub mod chain_extension;
Expand Down Expand Up @@ -409,6 +410,10 @@ pub mod pallet {
#[pallet::constant]
type MaxDebugBufferLen: Get<u32>;

/// The maximum length of the transient storage in bytes.
athei marked this conversation as resolved.
Show resolved Hide resolved
#[pallet::constant]
type MaxTransientStorageLen: Get<u32>;

/// Origin allowed to upload code.
///
/// By default, it is safe to set this to `EnsureSigned`, allowing anyone to upload contract
Expand Down Expand Up @@ -554,6 +559,7 @@ pub mod pallet {
type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>;
type MaxDelegateDependencies = MaxDelegateDependencies;
type MaxStorageKeyLen = ConstU32<128>;
type MaxTransientStorageLen = ConstU32<{ 1 * 1024 * 1024 }>;
type Migrations = ();
type Time = Self;
type Randomness = Self;
Expand Down Expand Up @@ -605,7 +611,11 @@ pub mod pallet {
// Max call depth is CallStack::size() + 1
let max_call_depth = u32::try_from(T::CallStack::size().saturating_add(1))
.expect("CallStack size is too big");

// Transient storage uses a BTreeMap, which has overhead compared to the raw size of
// key-value data. To ensure safety, a margin of 2x the raw key-value size is used.
let max_transient_storage_len = T::MaxTransientStorageLen::get()
.checked_mul(2)
.expect("MaxTransientStorageLen to big");
// Check that given configured `MaxCodeLen`, runtime heap memory limit can't be broken.
//
// In worst case, the decoded Wasm contract code would be `x16` times larger than the
Expand All @@ -615,7 +625,7 @@ pub mod pallet {
// Next, the pallet keeps the Wasm blob for each
// contract, hence we add up `MaxCodeLen` to the safety margin.
//
// Finally, the inefficiencies of the freeing-bump allocator
// The inefficiencies of the freeing-bump allocator
// being used in the client for the runtime memory allocations, could lead to possible
// memory allocations for contract code grow up to `x4` times in some extreme cases,
// which gives us total multiplier of `17*4` for `MaxCodeLen`.
Expand All @@ -624,17 +634,20 @@ pub mod pallet {
// memory should be available. Note that maximum allowed heap memory and stack size per
// each contract (stack frame) should also be counted.
//
// The pallet holds transient storage with a size up to `max_transient_storage_size`.
//
// Finally, we allow 50% of the runtime memory to be utilized by the contracts call
// stack, keeping the rest for other facilities, such as PoV, etc.
//
// This gives us the following formula:
//
// `(MaxCodeLen * 17 * 4 + MAX_STACK_SIZE + max_heap_size) * max_call_depth <
// max_runtime_mem/2`
// `(MaxCodeLen * 17 * 4 + MAX_STACK_SIZE + max_heap_size) * max_call_depth +
// max_transient_storage_size < max_runtime_mem/2`
//
// Hence the upper limit for the `MaxCodeLen` can be defined as follows:
let code_len_limit = max_runtime_mem
.saturating_div(2)
.saturating_sub(max_transient_storage_len)
.saturating_div(max_call_depth)
.saturating_sub(max_heap_size)
.saturating_sub(MAX_STACK_SIZE)
Expand Down Expand Up @@ -1235,6 +1248,8 @@ pub mod pallet {
DelegateDependencyAlreadyExists,
/// Can not add a delegate dependency to the code hash of the contract itself.
CannotAddSelfAsDelegateDependency,
/// Can not add more data to transient storage.
OutOfStorage,
athei marked this conversation as resolved.
Show resolved Hide resolved
}

/// A reason for the pallet contracts placing a hold on funds.
Expand Down
Loading
Loading