srml-contracts: Deferred actions (#3255)

* Switch to deferred actions

* Make restore_to a deferred action.

* Bump version.

* Review fixes.
This commit is contained in:
Sergei Pepyakin
2019-08-01 09:52:59 +02:00
committed by Gavin Wood
parent 780942192e
commit de02aee156
6 changed files with 337 additions and 171 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
// and set impl_version to equal spec_version. If only runtime
// implementation changes and behavior does not, then leave spec_version as
// is and increment impl_version.
spec_version: 123,
spec_version: 125,
impl_version: 125,
apis: RUNTIME_API_VERSIONS,
};
+92 -27
View File
@@ -90,6 +90,15 @@ pub trait Ext {
/// Notes a call dispatch.
fn note_dispatch_call(&mut self, call: CallOf<Self::T>);
/// Notes a restoration request.
fn note_restore_to(
&mut self,
dest: AccountIdOf<Self::T>,
code_hash: CodeHash<Self::T>,
rent_allowance: BalanceOf<Self::T>,
delta: Vec<StorageKey>,
);
/// Returns a reference to the account id of the caller.
fn caller(&self) -> &AccountIdOf<Self::T>;
@@ -254,13 +263,41 @@ impl<T: Trait> Token<T> for ExecFeeToken {
}
}
#[cfg_attr(any(feature = "std", test), derive(Debug, PartialEq, Eq, Clone))]
pub enum DeferredAction<T: Trait> {
DepositEvent {
/// A list of topics this event will be deposited with.
topics: Vec<T::Hash>,
/// The event to deposit.
event: Event<T>,
},
DispatchRuntimeCall {
/// The account id of the contract who dispatched this call.
origin: T::AccountId,
/// The call to dispatch.
call: T::Call,
},
RestoreTo {
/// The account id of the contract which is removed during the restoration and transfers
/// its storage to the restored contract.
donor: T::AccountId,
/// The account id of the restored contract.
dest: T::AccountId,
/// The code hash of the restored contract.
code_hash: CodeHash<T>,
/// The initial rent allowance to set.
rent_allowance: BalanceOf<T>,
/// The keys to delete upon restoration.
delta: Vec<StorageKey>,
},
}
pub struct ExecutionContext<'a, T: Trait + 'a, V, L> {
pub self_account: T::AccountId,
pub self_trie_id: Option<TrieId>,
pub overlay: OverlayAccountDb<'a, T>,
pub depth: usize,
pub events: Vec<IndexedEvent<T>>,
pub calls: Vec<(T::AccountId, T::Call)>,
pub deferred: Vec<DeferredAction<T>>,
pub config: &'a Config<T>,
pub vm: &'a V,
pub loader: &'a L,
@@ -284,8 +321,7 @@ where
self_account: origin,
overlay: OverlayAccountDb::<T>::new(&DirectAccountDb),
depth: 0,
events: Vec::new(),
calls: Vec::new(),
deferred: Vec::new(),
config: &cfg,
vm: &vm,
loader: &loader,
@@ -302,8 +338,7 @@ where
self_account: dest,
overlay: OverlayAccountDb::new(&self.overlay),
depth: self.depth + 1,
events: Vec::new(),
calls: Vec::new(),
deferred: Vec::new(),
config: self.config,
vm: self.vm,
loader: self.loader,
@@ -435,7 +470,7 @@ where
.into_result()?;
// Deposit an instantiation event.
nested.events.push(IndexedEvent {
nested.deferred.push(DeferredAction::DepositEvent {
event: RawEvent::Instantiated(caller.clone(), dest.clone()),
topics: Vec::new(),
});
@@ -464,15 +499,14 @@ where
-> Result<Vec<u8>, &'static str>
where F: FnOnce(&mut ExecutionContext<T, V, L>) -> Result<Vec<u8>, &'static str>
{
let (output_data, change_set, events, calls) = {
let (output_data, change_set, deferred) = {
let mut nested = self.nested(dest, trie_id);
let output_data = func(&mut nested)?;
(output_data, nested.overlay.into_change_set(), nested.events, nested.calls)
(output_data, nested.overlay.into_change_set(), nested.deferred)
};
self.overlay.commit(change_set);
self.events.extend(events);
self.calls.extend(calls);
self.deferred.extend(deferred);
Ok(output_data)
}
@@ -592,7 +626,7 @@ fn transfer<'a, T: Trait, V: Vm<T>, L: Loader<T>>(
if transactor != dest {
ctx.overlay.set_balance(transactor, new_from_balance);
ctx.overlay.set_balance(dest, new_to_balance);
ctx.events.push(IndexedEvent {
ctx.deferred.push(DeferredAction::DepositEvent {
event: RawEvent::Transfer(transactor.clone(), dest.clone(), value),
topics: Vec::new(),
});
@@ -656,11 +690,27 @@ where
.call(to.clone(), value, gas_meter, input_data, empty_output_buf)
}
/// Notes a call dispatch.
fn note_dispatch_call(&mut self, call: CallOf<Self::T>) {
self.ctx.calls.push(
(self.ctx.self_account.clone(), call)
);
self.ctx.deferred.push(DeferredAction::DispatchRuntimeCall {
origin: self.ctx.self_account.clone(),
call,
});
}
fn note_restore_to(
&mut self,
dest: AccountIdOf<Self::T>,
code_hash: CodeHash<Self::T>,
rent_allowance: BalanceOf<Self::T>,
delta: Vec<StorageKey>,
) {
self.ctx.deferred.push(DeferredAction::RestoreTo {
donor: self.ctx.self_account.clone(),
dest,
code_hash,
rent_allowance,
delta,
});
}
fn address(&self) -> &T::AccountId {
@@ -688,7 +738,7 @@ where
}
fn deposit_event(&mut self, topics: Vec<T::Hash>, data: Vec<u8>) {
self.ctx.events.push(IndexedEvent {
self.ctx.deferred.push(DeferredAction::DepositEvent {
topics,
event: RawEvent::Contract(self.ctx.self_account.clone(), data),
});
@@ -724,7 +774,7 @@ where
mod tests {
use super::{
BalanceOf, ExecFeeToken, ExecutionContext, Ext, Loader, EmptyOutputBuf, TransferFeeKind, TransferFeeToken,
Vm, VmExecResult, InstantiateReceipt, RawEvent, IndexedEvent,
Vm, VmExecResult, InstantiateReceipt, RawEvent, DeferredAction,
};
use crate::account_db::AccountDb;
use crate::gas::GasMeter;
@@ -741,6 +791,21 @@ mod tests {
const BOB: u64 = 2;
const CHARLIE: u64 = 3;
impl<'a, T, V, L> ExecutionContext<'a, T, V, L>
where T: crate::Trait
{
fn events(&self) -> Vec<DeferredAction<T>> {
self.deferred
.iter()
.filter(|action| match *action {
DeferredAction::DepositEvent { .. } => true,
_ => false,
})
.cloned()
.collect()
}
}
struct MockCtx<'a> {
ext: &'a mut dyn Ext<T = Test>,
input_data: &'a [u8],
@@ -1326,12 +1391,12 @@ mod tests {
// Check that the newly created account has the expected code hash and
// there are instantiation event.
assert_eq!(ctx.overlay.get_code_hash(&created_contract_address).unwrap(), dummy_ch);
assert_eq!(&ctx.events, &[
IndexedEvent {
assert_eq!(&ctx.events(), &[
DeferredAction::DepositEvent {
event: RawEvent::Transfer(ALICE, created_contract_address, 100),
topics: Vec::new(),
},
IndexedEvent {
DeferredAction::DepositEvent {
event: RawEvent::Instantiated(ALICE, created_contract_address),
topics: Vec::new(),
}
@@ -1384,16 +1449,16 @@ mod tests {
// Check that the newly created account has the expected code hash and
// there are instantiation event.
assert_eq!(ctx.overlay.get_code_hash(&created_contract_address).unwrap(), dummy_ch);
assert_eq!(&ctx.events, &[
IndexedEvent {
assert_eq!(&ctx.events(), &[
DeferredAction::DepositEvent {
event: RawEvent::Transfer(ALICE, BOB, 20),
topics: Vec::new(),
},
IndexedEvent {
DeferredAction::DepositEvent {
event: RawEvent::Transfer(BOB, created_contract_address, 15),
topics: Vec::new(),
},
IndexedEvent {
DeferredAction::DepositEvent {
event: RawEvent::Instantiated(BOB, created_contract_address),
topics: Vec::new(),
},
@@ -1441,8 +1506,8 @@ mod tests {
// The contract wasn't created so we don't expect to see an instantiation
// event here.
assert_eq!(&ctx.events, &[
IndexedEvent {
assert_eq!(&ctx.events(), &[
DeferredAction::DepositEvent {
event: RawEvent::Transfer(ALICE, BOB, 20),
topics: Vec::new(),
},
+100 -90
View File
@@ -621,84 +621,6 @@ decl_module! {
}
}
/// Allows a contract to restore a tombstone by giving its storage.
///
/// The contract that wants to restore (i.e. origin of the call, or `msg.sender` in Solidity terms) will compute a
/// tombstone with its storage and the given code_hash. If the computed tombstone
/// match the destination one, the destination contract is restored with the rent_allowance` specified,
/// while the origin sends all its funds to the destination and is removed.
fn restore_to(
origin,
dest: T::AccountId,
code_hash: CodeHash<T>,
rent_allowance: BalanceOf<T>,
delta: Vec<exec::StorageKey>
) {
let origin = ensure_signed(origin)?;
let mut origin_contract = <ContractInfoOf<T>>::get(&origin)
.and_then(|c| c.get_alive())
.ok_or("Cannot restore from inexisting or tombstone contract")?;
let current_block = <system::Module<T>>::block_number();
if origin_contract.last_write == Some(current_block) {
return Err("Origin TrieId written in the current block");
}
let dest_tombstone = <ContractInfoOf<T>>::get(&dest)
.and_then(|c| c.get_tombstone())
.ok_or("Cannot restore to inexisting or alive contract")?;
let last_write = if !delta.is_empty() {
Some(current_block)
} else {
origin_contract.last_write
};
let key_values_taken = delta.iter()
.filter_map(|key| {
child::get_raw(&origin_contract.trie_id, &blake2_256(key)).map(|value| {
child::kill(&origin_contract.trie_id, &blake2_256(key));
(key, value)
})
})
.collect::<Vec<_>>();
let tombstone = <TombstoneContractInfo<T>>::new(
// This operation is cheap enough because last_write (delta not included)
// is not this block as it has been checked earlier.
&runtime_io::child_storage_root(&origin_contract.trie_id)[..],
code_hash,
);
if tombstone != dest_tombstone {
for (key, value) in key_values_taken {
child::put_raw(&origin_contract.trie_id, &blake2_256(key), &value);
}
return Err("Tombstones don't match");
}
origin_contract.storage_size -= key_values_taken.iter()
.map(|(_, value)| value.len() as u32)
.sum::<u32>();
<ContractInfoOf<T>>::remove(&origin);
<ContractInfoOf<T>>::insert(&dest, ContractInfo::Alive(RawAliveContractInfo {
trie_id: origin_contract.trie_id,
storage_size: origin_contract.storage_size,
code_hash,
rent_allowance,
deduct_block: current_block,
last_write,
}));
let origin_free_balance = T::Currency::free_balance(&origin);
T::Currency::make_free_balance_be(&origin, <BalanceOf<T>>::zero());
T::Currency::deposit_creating(&dest, origin_free_balance);
}
fn on_finalize() {
GasSpent::kill();
}
@@ -727,14 +649,6 @@ impl<T: Trait> Module<T> {
if result.is_ok() {
// Commit all changes that made it thus far into the persistent storage.
DirectAccountDb.commit(ctx.overlay.into_change_set());
// Then deposit all events produced.
ctx.events.into_iter().for_each(|indexed_event| {
<system::Module<T>>::deposit_event_indexed(
&*indexed_event.topics,
<T as Trait>::Event::from(indexed_event.event).into(),
);
});
}
// Refund cost of the unused gas.
@@ -743,14 +657,110 @@ impl<T: Trait> Module<T> {
// can alter the balance of the caller.
gas::refund_unused_gas::<T>(&origin, gas_meter, imbalance);
// Dispatch every recorded call with an appropriate origin.
ctx.calls.into_iter().for_each(|(who, call)| {
let result = call.dispatch(RawOrigin::Signed(who.clone()).into());
Self::deposit_event(RawEvent::Dispatched(who, result.is_ok()));
// Execute deferred actions.
ctx.deferred.into_iter().for_each(|deferred| {
use self::exec::DeferredAction::*;
match deferred {
DepositEvent {
topics,
event,
} => <system::Module<T>>::deposit_event_indexed(
&*topics,
<T as Trait>::Event::from(event).into(),
),
DispatchRuntimeCall {
origin: who,
call,
} => {
let result = call.dispatch(RawOrigin::Signed(who.clone()).into());
Self::deposit_event(RawEvent::Dispatched(who, result.is_ok()));
}
RestoreTo {
donor,
dest,
code_hash,
rent_allowance,
delta,
} => {
let _result = Self::restore_to(donor, dest, code_hash, rent_allowance, delta);
}
}
});
result
}
fn restore_to(
origin: T::AccountId,
dest: T::AccountId,
code_hash: CodeHash<T>,
rent_allowance: BalanceOf<T>,
delta: Vec<exec::StorageKey>
) -> Result {
let mut origin_contract = <ContractInfoOf<T>>::get(&origin)
.and_then(|c| c.get_alive())
.ok_or("Cannot restore from inexisting or tombstone contract")?;
let current_block = <system::Module<T>>::block_number();
if origin_contract.last_write == Some(current_block) {
return Err("Origin TrieId written in the current block");
}
let dest_tombstone = <ContractInfoOf<T>>::get(&dest)
.and_then(|c| c.get_tombstone())
.ok_or("Cannot restore to inexisting or alive contract")?;
let last_write = if !delta.is_empty() {
Some(current_block)
} else {
origin_contract.last_write
};
let key_values_taken = delta.iter()
.filter_map(|key| {
child::get_raw(&origin_contract.trie_id, &blake2_256(key)).map(|value| {
child::kill(&origin_contract.trie_id, &blake2_256(key));
(key, value)
})
})
.collect::<Vec<_>>();
let tombstone = <TombstoneContractInfo<T>>::new(
// This operation is cheap enough because last_write (delta not included)
// is not this block as it has been checked earlier.
&runtime_io::child_storage_root(&origin_contract.trie_id)[..],
code_hash,
);
if tombstone != dest_tombstone {
for (key, value) in key_values_taken {
child::put_raw(&origin_contract.trie_id, &blake2_256(key), &value);
}
return Err("Tombstones don't match");
}
origin_contract.storage_size -= key_values_taken.iter()
.map(|(_, value)| value.len() as u32)
.sum::<u32>();
<ContractInfoOf<T>>::remove(&origin);
<ContractInfoOf<T>>::insert(&dest, ContractInfo::Alive(RawAliveContractInfo {
trie_id: origin_contract.trie_id,
storage_size: origin_contract.storage_size,
code_hash,
rent_allowance,
deduct_block: current_block,
last_write,
}));
let origin_free_balance = T::Currency::free_balance(&origin);
T::Currency::make_free_balance_be(&origin, <BalanceOf<T>>::zero());
T::Currency::deposit_creating(&dest, origin_free_balance);
Ok(())
}
}
decl_event! {
+27 -53
View File
@@ -1256,17 +1256,24 @@ fn default_rent_allowance_on_create() {
const CODE_RESTORATION: &str = r#"
(module
(import "env" "ext_set_storage" (func $ext_set_storage (param i32 i32 i32 i32)))
(import "env" "ext_dispatch_call" (func $ext_dispatch_call (param i32 i32)))
(import "env" "ext_restore_to" (func $ext_restore_to (param i32 i32 i32 i32 i32 i32 i32 i32)))
(import "env" "memory" (memory 1 1))
(func (export "call")
(call $ext_dispatch_call
;; Pointer to the start of the encoded call buffer
(i32.const 200)
;; The length of the encoded call buffer.
;;
;; NB: This is required to keep in sync with the values in `restoration`.
(i32.const 115)
(call $ext_restore_to
;; Pointer and length of the encoded dest buffer.
(i32.const 256)
(i32.const 8)
;; Pointer and length of the encoded code hash buffer
(i32.const 264)
(i32.const 32)
;; Pointer and length of the encoded rent_allowance buffer
(i32.const 296)
(i32.const 8)
;; Pointer and number of items in the delta buffer.
;; This buffer specifies multiple keys for removal before restoration.
(i32.const 100)
(i32.const 1)
)
)
(func (export "deploy")
@@ -1290,17 +1297,20 @@ const CODE_RESTORATION: &str = r#"
;; Data to restore
(data (i32.const 0) "\28")
;; ACL
;; Buffer that has ACL storage keys.
(data (i32.const 100) "\01")
;; Serialized version of `T::Call` that encodes a call to `restore_to` function. For more
;; details check out the `ENCODED_CALL_LITERAL`.
(data (i32.const 200)
"\01\05\02\00\00\00\00\00\00\00\69\ae\df\b4\f6\c1\c3\98\e9\7f\8a\52\04\de\0f\95\ad\5e\7d\c3"
"\54\09\60\be\ab\11\a8\6c\56\9f\bf\cf\32\00\00\00\00\00\00\00\08\01\00\00\00\00\00\00\00\00"
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\01\00\00\00\00\00\00"
"\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00\00"
;; Address of bob
(data (i32.const 256) "\02\00\00\00\00\00\00\00")
;; Code hash of SET_CODE
(data (i32.const 264)
"\69\ae\df\b4\f6\c1\c3\98\e9\7f\8a\52\04\de\0f\95"
"\ad\5e\7d\c3\54\09\60\be\ab\11\a8\6c\56\9f\bf\cf"
)
;; Rent allowance
(data (i32.const 296) "\32\00\00\00\00\00\00\00")
)
"#;
@@ -1325,45 +1335,9 @@ fn restoration_success() {
}
fn restoration(test_different_storage: bool, test_restore_to_with_dirty_storage: bool) {
let (set_rent_wasm, set_rent_code_hash) = compile_module::<Test>(CODE_SET_RENT).unwrap();
let (restoration_wasm, restoration_code_hash) =
compile_module::<Test>(CODE_RESTORATION).unwrap();
let (set_rent_wasm, set_rent_code_hash) = compile_module::<Test>(CODE_SET_RENT).unwrap();
let acl_key = {
let mut s = [0u8; 32];
s[0] = 1;
s
};
// This test can fail due to the encoding changes. In case it becomes too annoying
// let's rewrite so as we use this module controlled call or we serialize it in runtime.
let encoded = hex::encode(Encode::encode(&Call::Contract(super::Call::restore_to(
BOB,
set_rent_code_hash.into(),
<Test as balances::Trait>::Balance::from(50u32),
vec![acl_key, acl_key],
))));
// `ENCODED_CALL_LITERAL` is encoded `T::Call` represented as a byte array. There is an exact
// same copy of this (modulo hex notation differences) in `CODE_RESTORATION`.
//
// When this assert is triggered make sure that you update the literals here and in
// `CODE_RESTORATION`. Hopefully, we switch to automatic injection of the code.
const ENCODED_CALL_LITERAL: &str =
"0105020000000000000069aedfb4f6c1c398e97f8a5204de0f95ad5e7dc3540960beab11a86c569fbfcf320000\
0000000000080100000000000000000000000000000000000000000000000000000000000000010000000000000\
0000000000000000000000000000000000000000000000000";
assert_eq!(
encoded,
ENCODED_CALL_LITERAL,
"The literal was changed and requires updating here and in `CODE_RESTORATION`",
);
assert_eq!(
hex::decode(ENCODED_CALL_LITERAL).unwrap().len(),
115,
"The size of the literal was changed and requires updating in `CODE_RESTORATION`",
);
with_externalities(
&mut ExtBuilder::default().existential_deposit(50).build(),
+36
View File
@@ -185,6 +185,13 @@ mod tests {
#[derive(Debug, PartialEq, Eq)]
struct DispatchEntry(Call);
#[derive(Debug, PartialEq, Eq)]
struct RestoreEntry {
dest: u64,
code_hash: H256,
rent_allowance: u64,
delta: Vec<StorageKey>,
}
#[derive(Debug, PartialEq, Eq)]
struct CreateEntry {
code_hash: H256,
endowment: u64,
@@ -205,6 +212,7 @@ mod tests {
creates: Vec<CreateEntry>,
transfers: Vec<TransferEntry>,
dispatches: Vec<DispatchEntry>,
restores: Vec<RestoreEntry>,
// (topics, data)
events: Vec<(Vec<H256>, Vec<u8>)>,
next_account_id: u64,
@@ -262,6 +270,20 @@ mod tests {
fn note_dispatch_call(&mut self, call: Call) {
self.dispatches.push(DispatchEntry(call));
}
fn note_restore_to(
&mut self,
dest: u64,
code_hash: H256,
rent_allowance: u64,
delta: Vec<StorageKey>,
) {
self.restores.push(RestoreEntry {
dest,
code_hash,
rent_allowance,
delta,
});
}
fn caller(&self) -> &u64 {
&42
}
@@ -332,6 +354,20 @@ mod tests {
fn note_dispatch_call(&mut self, call: Call) {
(**self).note_dispatch_call(call)
}
fn note_restore_to(
&mut self,
dest: u64,
code_hash: H256,
rent_allowance: u64,
delta: Vec<StorageKey>,
) {
(**self).note_restore_to(
dest,
code_hash,
rent_allowance,
delta,
)
}
fn caller(&self) -> &u64 {
(**self).caller()
}
@@ -586,6 +586,87 @@ define_env!(Env, <E: Ext>,
Ok(())
},
// Record a request to restore the caller contract to the specified contract.
//
// At the finalization stage, i.e. when all changes from the extrinsic that invoked this
// contract are commited, this function will compute a tombstone hash from the caller's
// storage and the given code hash and if the hash matches the hash found in the tombstone at
// the specified address - kill the caller contract and restore the destination contract and set
// the specified `rent_allowance`. All caller's funds are transfered to the destination.
//
// This function doesn't perform restoration right away but defers it to the end of the
// transaction. If there is no tombstone in the destination address or if the hashes don't match
// then restoration is cancelled and no changes are made.
//
// `dest_ptr`, `dest_len` - the pointer and the length of a buffer that encodes `T::AccountId`
// with the address of the to be restored contract.
// `code_hash_ptr`, `code_hash_len` - the pointer and the length of a buffer that encodes
// a code hash of the to be restored contract.
// `rent_allowance_ptr`, `rent_allowance_len` - the pointer and the length of a buffer that
// encodes the rent allowance that must be set in the case of successful restoration.
// `delta_ptr` is the pointer to the start of a buffer that has `delta_count` storage keys
// laid out sequentially.
ext_restore_to(
ctx,
dest_ptr: u32,
dest_len: u32,
code_hash_ptr: u32,
code_hash_len: u32,
rent_allowance_ptr: u32,
rent_allowance_len: u32,
delta_ptr: u32,
delta_count: u32
) => {
let dest = {
let dest_buf = read_sandbox_memory(ctx, dest_ptr, dest_len)?;
<<E as Ext>::T as system::Trait>::AccountId::decode(&mut &dest_buf[..])
.ok_or_else(|| sandbox::HostError)?
};
let code_hash = {
let code_hash_buf = read_sandbox_memory(ctx, code_hash_ptr, code_hash_len)?;
<CodeHash<<E as Ext>::T>>::decode(&mut &code_hash_buf[..])
.ok_or_else(|| sandbox::HostError)?
};
let rent_allowance = {
let rent_allowance_buf = read_sandbox_memory(
ctx,
rent_allowance_ptr,
rent_allowance_len
)?;
BalanceOf::<<E as Ext>::T>::decode(&mut &rent_allowance_buf[..])
.ok_or_else(|| sandbox::HostError)?
};
let delta = {
// We don't use `with_capacity` here to not eagerly allocate the user specified amount
// of memory.
let mut delta = Vec::new();
let mut key_ptr = delta_ptr;
for _ in 0..delta_count {
const KEY_SIZE: usize = 32;
// Read the delta into the provided buffer and collect it into the buffer.
let mut delta_key: StorageKey = [0; KEY_SIZE];
read_sandbox_memory_into_buf(ctx, key_ptr, &mut delta_key)?;
delta.push(delta_key);
// Offset key_ptr to the next element.
key_ptr = key_ptr.checked_add(KEY_SIZE as u32).ok_or_else(|| sandbox::HostError)?;
}
delta
};
ctx.ext.note_restore_to(
dest,
code_hash,
rent_allowance,
delta,
);
Ok(())
},
// Returns the size of the scratch buffer.
//
// For more details on the scratch buffer see `ext_scratch_copy`.