Skip to content

Commit

Permalink
Introduce temporary storage in the host.
Browse files Browse the repository at this point in the history
Temporary storage is basically a contract-owned map that isn't persisted in the ledger, but may be used to share values between multiple contract calls within the same top-level invocation. This is useful to implement safe allowance-style patterns without the risk of the allowance to stay in the ledger indefinitely (e.g. to transfer token to a dynamic, arbitrary number of recipients).
  • Loading branch information
dmkozh committed Mar 17, 2023
1 parent 8910210 commit 225cf4f
Show file tree
Hide file tree
Showing 16 changed files with 208 additions and 10 deletions.
52 changes: 50 additions & 2 deletions soroban-env-common/env.json
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@
"type": "RawVal"
}
],
"return": "RawVal"
"return": "Void"
},
{
"export": "0",
Expand Down Expand Up @@ -837,7 +837,7 @@
"type": "RawVal"
}
],
"return": "RawVal"
"return": "Void"
},
{
"export": "3",
Expand All @@ -854,6 +854,54 @@
],
"return": "BytesObject",
"docs": "Deploys a contract from the current contract. `wasm_hash` must be a hash of the contract code that has already been installed on this network. `salt` is used to create a unique contract id."
},
{
"export": "4",
"name": "put_tmp_contract_data",
"args": [
{
"name": "k",
"type": "RawVal"
},
{
"name": "v",
"type": "RawVal"
}
],
"return": "Void"
},
{
"export": "5",
"name": "has_tmp_contract_data",
"args": [
{
"name": "k",
"type": "RawVal"
}
],
"return": "Bool"
},
{
"export": "6",
"name": "get_tmp_contract_data",
"args": [
{
"name": "k",
"type": "RawVal"
}
],
"return": "RawVal"
},
{
"export": "7",
"name": "del_tmp_contract_data",
"args": [
{
"name": "k",
"type": "RawVal"
}
],
"return": "Void"
}
]
},
Expand Down
2 changes: 1 addition & 1 deletion soroban-env-common/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@
pub const ENV_META_V0_SECTION_NAME: &'static str = "contractenvmetav0";

soroban_env_macros::generate_env_meta_consts!(
interface_version: 32,
interface_version: 33,
);
78 changes: 73 additions & 5 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ use soroban_env_common::{
U128Object, U32Val, U64Object, U64Val, VecObject, VmCaller, VmCallerEnv, Void, I256, U256,
};

use crate::budget::{AsBudget, Budget, CostType};
use crate::events::{
DebugError, DebugEvent, Events, InternalContractEvent, InternalEvent, InternalEventsBuffer,
};
Expand All @@ -29,6 +28,10 @@ use crate::{
auth::{AuthorizationManager, AuthorizationManagerSnapshot, RecordedAuthPayload},
native_contract::account_contract::ACCOUNT_CONTRACT_CHECK_AUTH_FN_NAME,
};
use crate::{
budget::{AsBudget, Budget, CostType},
storage::{TempStorage, TempStorageMap},
};

use crate::host_object::{HostMap, HostObject, HostObjectType, HostVec};
use crate::SymbolStr;
Expand Down Expand Up @@ -62,6 +65,7 @@ use crate::Compare;
#[derive(Clone)]
struct RollbackPoint {
storage: StorageMap,
temp_storage: TempStorageMap,
events: usize,
auth: Option<AuthorizationManagerSnapshot>,
}
Expand Down Expand Up @@ -160,6 +164,7 @@ pub(crate) struct HostImpl {
ledger: RefCell<Option<LedgerInfo>>,
objects: RefCell<Vec<HostObject>>,
storage: RefCell<Storage>,
temp_storage: RefCell<TempStorage>,
pub(crate) context: RefCell<Vec<Frame>>,
// Note: budget is refcounted and is _not_ deep-cloned when you call HostImpl::deep_clone,
// mainly because it's not really possible to achieve (the same budget is connected to many
Expand Down Expand Up @@ -238,6 +243,7 @@ impl Host {
ledger: RefCell::new(None),
objects: Default::default(),
storage: RefCell::new(storage),
temp_storage: Default::default(),
context: Default::default(),
budget: budget.clone(),
events: Default::default(),
Expand Down Expand Up @@ -431,6 +437,7 @@ impl Host {
self.0.context.borrow_mut().push(frame);
Ok(RollbackPoint {
storage: self.0.storage.borrow().map.clone(),
temp_storage: self.0.temp_storage.borrow().map.clone(),
events: self.0.events.borrow().vec.len(),
auth: auth_snapshot,
})
Expand Down Expand Up @@ -2303,7 +2310,7 @@ impl VmCallerEnv for Host {
_vmcaller: &mut VmCaller<Host>,
k: RawVal,
v: RawVal,
) -> Result<RawVal, HostError> {
) -> Result<Void, HostError> {
let key = self.contract_data_key_from_rawval(k)?;
let data = LedgerEntryData::ContractData(ContractDataEntry {
contract_id: self.get_current_contract_id_internal()?,
Expand All @@ -2315,7 +2322,7 @@ impl VmCallerEnv for Host {
&Host::ledger_entry_from_data(data),
self.as_budget(),
)?;
Ok(().into())
Ok(RawVal::VOID)
}

// Notes on metering: covered by components
Expand Down Expand Up @@ -2355,10 +2362,10 @@ impl VmCallerEnv for Host {
&self,
_vmcaller: &mut VmCaller<Host>,
k: RawVal,
) -> Result<RawVal, HostError> {
) -> Result<Void, HostError> {
let key = self.contract_data_key_from_rawval(k)?;
self.0.storage.borrow_mut().del(&key, self.as_budget())?;
Ok(().into())
Ok(RawVal::VOID)
}

// Notes on metering: covered by the components.
Expand All @@ -2377,6 +2384,67 @@ impl VmCallerEnv for Host {
self.create_contract_with_id_preimage(code, id_preimage)
}

// Notes on metering: covered by components
fn put_tmp_contract_data(
&self,
_vmcaller: &mut VmCaller<Host>,
k: RawVal,
v: RawVal,
) -> Result<Void, HostError> {
let key = self.from_host_val(k)?;
self.0.temp_storage.borrow_mut().put(
self.get_current_contract_id_internal()?.0,
key,
v,
self.as_budget(),
)?;
Ok(RawVal::VOID)
}

// Notes on metering: covered by components
fn has_tmp_contract_data(
&self,
_vmcaller: &mut VmCaller<Host>,
k: RawVal,
) -> Result<Bool, HostError> {
let key = self.from_host_val(k)?;
let res = self.0.temp_storage.borrow_mut().has(
self.get_current_contract_id_internal()?.0,
key,
self.as_budget(),
)?;
Ok(RawVal::from_bool(res))
}

// Notes on metering: covered by components
fn get_tmp_contract_data(
&self,
_vmcaller: &mut VmCaller<Host>,
k: RawVal,
) -> Result<RawVal, HostError> {
let key = self.from_host_val(k)?;
self.0.temp_storage.borrow_mut().get(
self.get_current_contract_id_internal()?.0,
key,
self.as_budget(),
)
}

// Notes on metering: covered by components
fn del_tmp_contract_data(
&self,
_vmcaller: &mut VmCaller<Host>,
k: RawVal,
) -> Result<Void, HostError> {
let key = self.from_host_val(k)?;
self.0.temp_storage.borrow_mut().del(
self.get_current_contract_id_internal()?.0,
key,
self.as_budget(),
)?;
Ok(RawVal::VOID)
}

// Notes on metering: here covers the args unpacking. The actual VM work is changed at lower layers.
fn call(
&self,
Expand Down
86 changes: 84 additions & 2 deletions soroban-env-host/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,24 @@
//! - [Env::put_contract_data](crate::Env::put_contract_data)
//! - [Env::del_contract_data](crate::Env::del_contract_data)

use std::cmp::Ordering;
use std::rc::Rc;

use soroban_env_common::Compare;
use soroban_env_common::{Compare, RawVal};

use crate::budget::Budget;
use crate::xdr::{LedgerEntry, LedgerKey, ScHostStorageErrorCode};
use crate::xdr::{LedgerEntry, LedgerKey, ScHostStorageErrorCode, ScVal};
use crate::Host;
use crate::{host::metered_map::MeteredOrdMap, HostError};

pub type FootprintMap = MeteredOrdMap<Rc<LedgerKey>, AccessType, Budget>;
pub type StorageMap = MeteredOrdMap<Rc<LedgerKey>, Option<Rc<LedgerEntry>>, Budget>;
// Alias for the `RawVal` that has the cheap 'shallow' comparison defined - it
// just compares the payloads without looking at the value semantics.
// This is used in `TempStorageMap` to make sure that we don't do expensive
// value comparisons.
pub type ShallowComparableRawVal = RawVal;
pub type TempStorageMap = MeteredOrdMap<([u8; 32], ScVal), ShallowComparableRawVal, Budget>;

/// A helper type used by [Footprint] to designate which ways
/// a given [LedgerKey] is accessed, or is allowed to be accessed,
Expand Down Expand Up @@ -294,6 +301,81 @@ impl Storage {
}
}

impl Compare<ShallowComparableRawVal> for Budget {
type Error = HostError;

fn compare(
&self,
a: &ShallowComparableRawVal,
b: &ShallowComparableRawVal,
) -> Result<Ordering, Self::Error> {
Ok(a.get_payload().cmp(&b.get_payload()))
}
}

/// A special-purpose map from arbitrary contract-owned values to arbitrary
/// values.
///
/// Since `TempStorage` stores `RawVal`s, it has to be attributed to the host
/// instance and can't be transferred between host instances.
///
/// Semantically, this is similar to the `Storage`, but it is never persisted
/// and hence doesn't need to support the footprints and convert keys to the
/// ledger-compatibile types.
#[derive(Clone, Default)]
pub struct TempStorage {
pub map: TempStorageMap,
}

impl TempStorage {
pub fn get(
&self,
contract_id: [u8; 32],
key: ScVal,
budget: &Budget,
) -> Result<RawVal, HostError> {
match self.map.get(&(contract_id, key), budget)? {
None => Err(ScHostStorageErrorCode::MissingKeyInGet.into()),
Some(val) => Ok(*val),
}
}

pub fn put(
&mut self,
contract_id: [u8; 32],
key: ScVal,
val: RawVal,
budget: &Budget,
) -> Result<(), HostError> {
self.map = self.map.insert((contract_id, key), val, budget)?;
Ok(())
}

pub fn del(
&mut self,
contract_id: [u8; 32],
key: ScVal,
budget: &Budget,
) -> Result<(), HostError> {
match self.map.remove(&(contract_id, key), budget)? {
Some((new_self, _)) => {
self.map = new_self;
}
None => (),
};
Ok(())
}

pub fn has(
&mut self,
contract_id: [u8; 32],
key: ScVal,
budget: &Budget,
) -> Result<bool, HostError> {
self.map.contains_key(&(contract_id, key), budget)
}
}

#[cfg(test)]
mod test_footprint {

Expand Down
Binary file modified soroban-test-wasms/wasm-workspace/opt/auth_test_contract.wasm
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_add_i32.wasm
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_complex.wasm
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_contract_data.wasm
Binary file not shown.
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_fannkuch.wasm
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_fib.wasm
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_hostile.wasm
Binary file not shown.
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_linear_memory.wasm
Binary file not shown.
Binary file not shown.
Binary file modified soroban-test-wasms/wasm-workspace/opt/example_vec.wasm
Binary file not shown.

0 comments on commit 225cf4f

Please sign in to comment.