diff --git a/Cargo.lock b/Cargo.lock index 683fb8df0..8ba2b0f78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,6 +1672,7 @@ dependencies = [ "near-primitives-core", "near-sdk-macros", "near-vm-logic", + "once_cell", "quickcheck", "rand 0.7.3", "rand_xorshift", @@ -1901,9 +1902,9 @@ checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" [[package]] name = "once_cell" -version = "1.7.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10acf907b94fc1b1a152d08ef97e7759650268cf986bf127f387e602b02c7e5a" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "opaque-debug" diff --git a/near-sdk/Cargo.toml b/near-sdk/Cargo.toml index e7525805f..04f08770b 100644 --- a/near-sdk/Cargo.toml +++ b/near-sdk/Cargo.toml @@ -29,6 +29,9 @@ near-primitives-core = "=0.4.0" # Export dependencies for contracts wee_alloc = { version = "0.4.5", default-features = false, features = [] } +# Used for caching, might be worth porting only functionality needed. +once_cell = { version = "1.7.2", optional = true, default-features = false } + [dev-dependencies] rand = "0.7.2" trybuild = "1.0" @@ -38,3 +41,4 @@ quickcheck = "0.9.2" [features] expensive-debug = [] +unstable = ["once_cell"] diff --git a/near-sdk/src/lib.rs b/near-sdk/src/lib.rs index b7a2548c4..77fd7f2c6 100644 --- a/near-sdk/src/lib.rs +++ b/near-sdk/src/lib.rs @@ -6,6 +6,9 @@ pub use near_sdk_macros::{ serializer, BorshStorageKey, PanicOnDefault, }; +#[cfg(feature = "unstable")] +pub mod store; + pub mod collections; mod environment; pub use environment::env; diff --git a/near-sdk/src/store/lazy/impls.rs b/near-sdk/src/store/lazy/impls.rs new file mode 100644 index 000000000..582d24bba --- /dev/null +++ b/near-sdk/src/store/lazy/impls.rs @@ -0,0 +1,91 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::Lazy; + +impl Drop for Lazy +where + T: BorshSerialize, +{ + fn drop(&mut self) { + self.flush() + } +} + +impl core::ops::Deref for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + Self::get(self) + } +} + +impl core::ops::DerefMut for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + Self::get_mut(self) + } +} + +impl core::cmp::PartialEq for Lazy +where + T: PartialEq + BorshSerialize + BorshDeserialize, +{ + fn eq(&self, other: &Self) -> bool { + PartialEq::eq(self.get(), other.get()) + } +} + +impl core::cmp::Eq for Lazy where T: Eq + BorshSerialize + BorshDeserialize {} + +impl core::cmp::PartialOrd for Lazy +where + T: PartialOrd + BorshSerialize + BorshDeserialize, +{ + fn partial_cmp(&self, other: &Self) -> Option { + PartialOrd::partial_cmp(self.get(), other.get()) + } + fn lt(&self, other: &Self) -> bool { + PartialOrd::lt(self.get(), other.get()) + } + fn le(&self, other: &Self) -> bool { + PartialOrd::le(self.get(), other.get()) + } + fn ge(&self, other: &Self) -> bool { + PartialOrd::ge(self.get(), other.get()) + } + fn gt(&self, other: &Self) -> bool { + PartialOrd::gt(self.get(), other.get()) + } +} + +impl core::cmp::Ord for Lazy +where + T: core::cmp::Ord + BorshSerialize + BorshDeserialize, +{ + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + Ord::cmp(self.get(), other.get()) + } +} + +impl core::convert::AsRef for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + fn as_ref(&self) -> &T { + Self::get(self) + } +} + +impl core::convert::AsMut for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + fn as_mut(&mut self) -> &mut T { + Self::get_mut(self) + } +} diff --git a/near-sdk/src/store/lazy/mod.rs b/near-sdk/src/store/lazy/mod.rs new file mode 100644 index 000000000..18fc1f265 --- /dev/null +++ b/near-sdk/src/store/lazy/mod.rs @@ -0,0 +1,185 @@ +//! A persistent lazy storage value. Stores a value for a given key. +//! Example: +//! If the underlying value is large, e.g. the contract needs to store an image, but it doesn't need +//! to have access to this image at regular calls, then the contract can wrap this image into +//! [`Lazy`] and it will not be deserialized until requested. + +mod impls; + +use borsh::{BorshDeserialize, BorshSerialize}; +use once_cell::unsync::OnceCell; + +use crate::env; +use crate::utils::{CacheEntry, EntryState}; +use crate::IntoStorageKey; + +const ERR_VALUE_SERIALIZATION: &[u8] = b"Cannot serialize value with Borsh"; +const ERR_VALUE_DESERIALIZATION: &[u8] = b"Cannot deserialize value with Borsh"; +const ERR_NOT_FOUND: &[u8] = b"No value found for the given key"; +const ERR_DELETED: &[u8] = b"The Lazy cell's value has been deleted. Verify the key has not been\ + deleted manually."; + +fn expect_key_exists(val: Option) -> T { + val.unwrap_or_else(|| env::panic(ERR_NOT_FOUND)) +} + +fn expect_consistent_state(val: Option) -> T { + val.unwrap_or_else(|| env::panic(ERR_DELETED)) +} + +fn load_and_deserialize(key: &[u8]) -> CacheEntry +where + T: BorshDeserialize, +{ + let bytes = expect_key_exists(env::storage_read(key)); + let val = T::try_from_slice(&bytes).unwrap_or_else(|_| env::panic(ERR_VALUE_DESERIALIZATION)); + CacheEntry::new_cached(Some(val)) +} + +fn serialize_and_store(key: &[u8], value: &T) +where + T: BorshSerialize, +{ + let serialized = value.try_to_vec().unwrap_or_else(|_| env::panic(ERR_VALUE_SERIALIZATION)); + env::storage_write(&key, &serialized); +} + +/// An persistent lazily loaded value, that stores a value in the storage. +/// +/// This will only write to the underlying store if the value has changed, and will only read the +/// existing value from storage once. +/// +/// # Examples +/// ``` +/// use near_sdk::collections::Lazy; +/// +///# near_sdk::test_utils::test_env::setup(); +/// let mut a = Lazy::new(b"a", "test string".to_string()); +/// assert_eq!(*a, "test string"); +/// +/// *a = "new string".to_string(); +/// assert_eq!(a.get(), "new string"); +/// ``` +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct Lazy +where + T: BorshSerialize, +{ + /// Key bytes to index the contract's storage. + storage_key: Vec, + #[borsh_skip] + /// Cached value which is lazily loaded and deserialized from storage. + cache: OnceCell>, +} + +impl Lazy +where + T: BorshSerialize, +{ + pub fn new(key: S, value: T) -> Self + where + S: IntoStorageKey, + { + Self { + storage_key: key.into_storage_key(), + cache: OnceCell::from(CacheEntry::new_modified(Some(value))), + } + } + + /// Updates the value with a new value. This does not load the current value from storage. + pub fn set(&mut self, value: T) { + if let Some(v) = self.cache.get_mut() { + *v.value_mut() = Some(value); + } else { + self.cache + .set(CacheEntry::new_modified(Some(value))) + .ok() + .expect("cache is checked to not be filled above"); + } + } + + /// Writes any changes to the value to storage. This will automatically be done when the + /// value is dropped through [`Drop`] so this should only be used when the changes need to be + /// reflected in the underlying storage before then. + pub fn flush(&mut self) { + if let Some(v) = self.cache.get_mut() { + if v.is_modified() { + // Value was modified, serialize and put the serialized bytes in storage. + let value = expect_consistent_state(v.value().as_ref()); + serialize_and_store(&self.storage_key, value); + + // Replaces cache entry state to cached because the value in memory matches the + // stored value. This avoids writing the same value twice. + v.replace_state(EntryState::Cached); + } + } + } +} + +impl Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + /// Returns a reference to the lazily loaded storage value. + /// The load from storage only happens once, and if the value is already cached, it will not + /// be reloaded. + /// + /// This function will panic if the cache is not loaded and the value at the key does not exist. + pub fn get(&self) -> &T { + let entry = self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); + + expect_consistent_state(entry.value().as_ref()) + } + + /// Returns a reference to the lazily loaded storage value. + /// The load from storage only happens once, and if the value is already cached, it will not + /// be reloaded. + /// + /// This function will panic if the cache is not loaded and the value at the key does not exist. + pub fn get_mut(&mut self) -> &mut T { + self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); + let entry = self.cache.get_mut().expect("cell should be filled above"); + + expect_consistent_state(entry.value_mut().as_mut()) + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod tests { + use super::*; + + use crate::test_utils::test_env; + + #[test] + pub fn test_lazy() { + test_env::setup(); + let mut a = Lazy::new(b"a", 8u32); + assert_eq!(a.get(), &8); + + assert!(!env::storage_has_key(b"a")); + a.flush(); + assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 8); + + a.set(42); + + // Value in storage will still be 8 until the value is flushed + assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 8); + assert_eq!(*a, 42); + + *a = 30; + let serialized = a.try_to_vec().unwrap(); + drop(a); + assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 30); + + let lazy_loaded = Lazy::::try_from_slice(&serialized).unwrap(); + assert!(lazy_loaded.cache.get().is_none()); + + let b = Lazy::new(b"b", 30); + assert!(!env::storage_has_key(b"b")); + + // A value that is not stored in storage yet and one that has not been loaded yet can + // be checked for equality. + assert_eq!(lazy_loaded, b); + } +} diff --git a/near-sdk/src/store/mod.rs b/near-sdk/src/store/mod.rs new file mode 100644 index 000000000..144bb17ab --- /dev/null +++ b/near-sdk/src/store/mod.rs @@ -0,0 +1,2 @@ +mod lazy; +pub use lazy::Lazy; diff --git a/near-sdk/src/utils/cache_entry.rs b/near-sdk/src/utils/cache_entry.rs new file mode 100644 index 000000000..16cb922b0 --- /dev/null +++ b/near-sdk/src/utils/cache_entry.rs @@ -0,0 +1,69 @@ +#[derive(Clone, Debug)] +pub(crate) struct CacheEntry { + value: Option, + state: EntryState, +} + +impl CacheEntry { + pub fn new(value: Option, state: EntryState) -> Self { + Self { value, state } + } + + pub fn new_cached(value: Option) -> Self { + Self::new(value, EntryState::Cached) + } + + pub fn new_modified(value: Option) -> Self { + Self::new(value, EntryState::Modified) + } + + pub fn value(&self) -> &Option { + &self.value + } + + pub fn value_mut(&mut self) -> &mut Option { + self.state = EntryState::Modified; + &mut self.value + } + + #[allow(dead_code)] + pub fn into_value(self) -> Option { + self.value + } + + /// Replaces the current value with a new one. This changes the state of the cell to mutated + /// if either the old or new value is [`Some`]. + #[allow(dead_code)] + pub fn replace(&mut self, value: Option) -> Option { + let old_value = core::mem::replace(&mut self.value, value); + + if self.value.is_some() || old_value.is_some() { + // Set modified if both values are not `None` + self.state = EntryState::Modified; + } + + old_value + } + + /// Replaces the state of the cache entry and returns the previous value. + pub fn replace_state(&mut self, state: EntryState) -> EntryState { + core::mem::replace(&mut self.state, state) + } + + /// Returns true if the entry has been modified + pub fn is_modified(&self) -> bool { + matches!(self.state, EntryState::Modified) + } + + #[allow(dead_code)] + /// Returns true if the entry state has not been changed. + pub fn is_cached(&self) -> bool { + !self.is_modified() + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) enum EntryState { + Modified, + Cached, +} diff --git a/near-sdk/src/utils/mod.rs b/near-sdk/src/utils/mod.rs index 7b3526f14..2a14d53a9 100644 --- a/near-sdk/src/utils/mod.rs +++ b/near-sdk/src/utils/mod.rs @@ -1,5 +1,10 @@ pub(crate) mod storage_key_impl; +#[cfg(feature = "unstable")] +mod cache_entry; +#[cfg(feature = "unstable")] +pub(crate) use cache_entry::{CacheEntry, EntryState}; + use crate::{env, AccountId, PromiseResult}; /// Helper macro to log a message through [`env::log`].