Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 soroban-sdk-macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ sha2 = "0.10.7"
heck = "0.5.0"

[features]
next = ["soroban-env-common/next"]
testutils = []
experimental_spec_shaking_v2 = []
1 change: 1 addition & 0 deletions soroban-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ trybuild = "1.0.115"

[features]
alloc = []
next = ["soroban-sdk-macros/next", "soroban-env-guest/next", "soroban-env-host/next"]
testutils = ["soroban-sdk-macros/testutils", "soroban-env-host/testutils", "soroban-ledger-snapshot/testutils", "dep:ed25519-dalek", "dep:arbitrary", "dep:derive_arbitrary", "dep:ctor", "dep:soroban-ledger-snapshot"]
experimental_spec_shaking_v2 = ["soroban-sdk-macros/experimental_spec_shaking_v2"]
docs = []
Expand Down
216 changes: 212 additions & 4 deletions soroban-sdk/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Storage contains types for storing data for the currently executing contract.
use core::fmt::Debug;

#[cfg(feature = "next")]
use crate::{env::internal::AddressObject, Address};
use crate::{
env::internal::{self, ContractTtlExtension, StorageType, Val},
unwrap::{UnwrapInfallible, UnwrapOptimized},
Expand All @@ -9,8 +11,9 @@ use crate::{

/// Storage stores and retrieves data for the currently executing contract.
///
/// All data stored can only be queried and modified by the contract that stores
/// it. Contracts cannot query or modify data stored by other contracts.
/// By default, data can only be queried and modified by the contract that stores
/// it. The `next` feature also exposes read-only external storage methods that
/// can query another contract's storage without invoking that contract.
///
/// There are three types of storage - Temporary, Persistent, and Instance.
///
Expand Down Expand Up @@ -181,8 +184,7 @@ impl Storage {
V: TryFromVal<Env, Val>,
{
let key = key.into_val(&self.env);
if self.has_internal(key, storage_type) {
let rv = self.get_internal(key, storage_type);
if let Some(rv) = self.try_get_internal(key, storage_type) {
Some(V::try_from_val(&self.env, &rv).unwrap_optimized())
} else {
None
Expand Down Expand Up @@ -326,6 +328,134 @@ impl Storage {
fn get_internal(&self, key: Val, storage_type: StorageType) -> Val {
internal::Env::get_contract_data(&self.env, key, storage_type).unwrap_infallible()
}

#[cfg(all(target_family = "wasm", feature = "next"))]
fn try_get_internal(&self, key: Val, storage_type: StorageType) -> Option<Val> {
#[link(wasm_import_module = "l")]
extern "C" {
#[link_name = "h"]
fn try_get_contract_data(k: Val, t: StorageType, has_pos: internal::U32Val) -> Val;
}

let mut has = Val::VOID.to_val();
let has_pos = internal::U32Val::from((&mut has as *mut Val) as u32);
Comment on lines +332 to +341
let value = unsafe { try_get_contract_data(key, storage_type, has_pos) };
if bool::from(internal::Bool::try_from_val(&self.env, &has).unwrap_optimized()) {
Some(value)
} else {
None
}
}

#[cfg(not(all(target_family = "wasm", feature = "next")))]
fn try_get_internal(&self, key: Val, storage_type: StorageType) -> Option<Val> {
if self.has_internal(key, storage_type) {
Some(self.get_internal(key, storage_type))
} else {
None
}
}

/// Returns if the provided contract stores a value under the given key.
///
/// This is read-only. It records the target contract data key in the
/// transaction footprint and does not invoke the target contract.
#[cfg(feature = "next")]
#[inline(always)]
pub(crate) fn has_external<K>(
&self,
contract: &Address,
key: &K,
storage_type: StorageType,
) -> bool
where
K: IntoVal<Env, Val>,
{
self.has_external_internal(contract.to_object(), key.into_val(&self.env), storage_type)
}

/// Returns the value stored by the provided contract under the given key.
///
/// Returns `None` when the value is missing. This is read-only and does not
/// invoke the target contract. External instance storage reads load the
/// target contract's whole instance entry.
#[cfg(feature = "next")]
#[inline(always)]
pub(crate) fn get_external<K, V>(
&self,
contract: &Address,
key: &K,
storage_type: StorageType,
) -> Option<V>
where
K: IntoVal<Env, Val>,
V: TryFromVal<Env, Val>,
{
let contract = contract.to_object();
let key = key.into_val(&self.env);
if self.has_external_internal(contract, key, storage_type) {
let rv = self.get_external_internal(contract, key, storage_type);
Some(V::try_from_val(&self.env, &rv).unwrap_optimized())
} else {
None
}
}

#[cfg(all(target_family = "wasm", feature = "next"))]
fn has_external_internal(
&self,
contract: AddressObject,
key: Val,
storage_type: StorageType,
) -> bool {
#[link(wasm_import_module = "l")]
extern "C" {
#[link_name = "i"]
fn has_external_contract_data(
contract: AddressObject,
k: Val,
t: StorageType,
) -> internal::Bool;
}

unsafe { has_external_contract_data(contract, key, storage_type) }.into()
}

#[cfg(all(not(target_family = "wasm"), feature = "next"))]
fn has_external_internal(
&self,
_contract: AddressObject,
_key: Val,
_storage_type: StorageType,
) -> bool {
panic!("external storage reads require protocol 28 host support")
}
Comment on lines +424 to +432

#[cfg(all(target_family = "wasm", feature = "next"))]
fn get_external_internal(
&self,
contract: AddressObject,
key: Val,
storage_type: StorageType,
) -> Val {
#[link(wasm_import_module = "l")]
extern "C" {
#[link_name = "j"]
fn get_external_contract_data(contract: AddressObject, k: Val, t: StorageType) -> Val;
}

unsafe { get_external_contract_data(contract, key, storage_type) }
}

#[cfg(all(not(target_family = "wasm"), feature = "next"))]
fn get_external_internal(
&self,
_contract: AddressObject,
_key: Val,
_storage_type: StorageType,
) -> Val {
panic!("external storage reads require protocol 28 host support")
}
Comment on lines +450 to +458
}

pub struct Persistent {
Expand All @@ -349,6 +479,32 @@ impl Persistent {
self.storage.get(key, StorageType::Persistent)
}

/// Returns if the provided contract stores a persistent value under `key`.
///
/// This is read-only and does not invoke the target contract.
#[cfg(feature = "next")]
pub fn has_external<K>(&self, contract: &Address, key: &K) -> bool
Comment on lines +482 to +486
where
K: IntoVal<Env, Val>,
{
self.storage
.has_external(contract, key, StorageType::Persistent)
}

/// Returns a persistent value stored by the provided contract.
///
/// This is read-only and does not invoke the target contract.
#[cfg(feature = "next")]
pub fn get_external<K, V>(&self, contract: &Address, key: &K) -> Option<V>
where
V::Error: Debug,
K: IntoVal<Env, Val>,
V: TryFromVal<Env, Val>,
{
self.storage
.get_external(contract, key, StorageType::Persistent)
}

pub fn set<K, V>(&self, key: &K, val: &V)
where
K: IntoVal<Env, Val>,
Expand Down Expand Up @@ -464,6 +620,32 @@ impl Temporary {
self.storage.get(key, StorageType::Temporary)
}

/// Returns if the provided contract stores a temporary value under `key`.
///
/// This is read-only and does not invoke the target contract.
#[cfg(feature = "next")]
pub fn has_external<K>(&self, contract: &Address, key: &K) -> bool
where
K: IntoVal<Env, Val>,
{
self.storage
.has_external(contract, key, StorageType::Temporary)
}

/// Returns a temporary value stored by the provided contract.
///
/// This is read-only and does not invoke the target contract.
#[cfg(feature = "next")]
pub fn get_external<K, V>(&self, contract: &Address, key: &K) -> Option<V>
where
V::Error: Debug,
K: IntoVal<Env, Val>,
V: TryFromVal<Env, Val>,
{
self.storage
.get_external(contract, key, StorageType::Temporary)
}

pub fn set<K, V>(&self, key: &K, val: &V)
where
K: IntoVal<Env, Val>,
Expand Down Expand Up @@ -553,6 +735,32 @@ impl Instance {
self.storage.get(key, StorageType::Instance)
}

/// Returns if the provided contract stores an instance value under `key`.
///
/// This is read-only and loads the target contract's whole instance entry.
#[cfg(feature = "next")]
pub fn has_external<K>(&self, contract: &Address, key: &K) -> bool
where
K: IntoVal<Env, Val>,
{
self.storage
.has_external(contract, key, StorageType::Instance)
}

/// Returns an instance value stored by the provided contract.
///
/// This is read-only and loads the target contract's whole instance entry.
#[cfg(feature = "next")]
pub fn get_external<K, V>(&self, contract: &Address, key: &K) -> Option<V>
where
V::Error: Debug,
K: IntoVal<Env, Val>,
V: TryFromVal<Env, Val>,
{
self.storage
.get_external(contract, key, StorageType::Instance)
}

pub fn set<K, V>(&self, key: &K, val: &V)
where
K: IntoVal<Env, Val>,
Expand Down
20 changes: 20 additions & 0 deletions soroban-sdk/tests/external_storage_next.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#![cfg(feature = "next")]

use soroban_sdk::{Address, Env, Symbol};

#[allow(dead_code)]
fn external_storage_methods_type_check(
env: Env,
contract: Address,
key: Symbol,
) -> (bool, Option<u64>, bool, Option<u64>, bool, Option<u64>) {
let storage = env.storage();
(
storage.persistent().has_external(&contract, &key),
storage.persistent().get_external(&contract, &key),
storage.temporary().has_external(&contract, &key),
storage.temporary().get_external(&contract, &key),
storage.instance().has_external(&contract, &key),
storage.instance().get_external(&contract, &key),
)
}