use core::future::Future;
use freenet_stdlib::prelude::*;
use moka::sync::Cache as MokaCache;
pub(crate) const MAX_STATE_SIZE: usize = 50 * 1024 * 1024;
#[derive(thiserror::Error, Debug)]
pub enum StateStoreError {
#[error(transparent)]
Any(#[from] anyhow::Error),
#[error("missing contract: {0}")]
MissingContract(ContractKey),
#[error("contract state too large for {key}: {size} bytes exceeds limit of {limit} bytes")]
StateTooLarge {
key: ContractKey,
size: usize,
limit: usize,
},
}
impl From<StateStoreError> for crate::wasm_runtime::ContractError {
fn from(value: StateStoreError) -> Self {
match value {
StateStoreError::Any(err) => {
crate::wasm_runtime::ContractError::from(anyhow::format_err!(err))
}
err @ StateStoreError::MissingContract(_) => {
crate::wasm_runtime::ContractError::from(anyhow::format_err!(err))
}
err @ StateStoreError::StateTooLarge { .. } => {
crate::wasm_runtime::ContractError::from(anyhow::format_err!(err))
}
}
}
}
pub trait StateStorage {
type Error;
fn store(
&self,
key: ContractKey,
state: WrappedState,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn store_params(
&self,
key: ContractKey,
state: Parameters<'static>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn get(
&self,
key: &ContractKey,
) -> impl Future<Output = Result<Option<WrappedState>, Self::Error>> + Send;
fn get_params<'a>(
&'a self,
key: &'a ContractKey,
) -> impl Future<Output = Result<Option<Parameters<'static>>, Self::Error>> + Send + 'a;
}
#[derive(Clone)]
pub struct StateStore<S: StateStorage> {
state_mem_cache: Option<MokaCache<ContractKey, WrappedState>>,
store: S,
}
impl<S> StateStore<S>
where
S: StateStorage + Send + 'static,
<S as StateStorage>::Error: Into<anyhow::Error>,
{
pub fn new(store: S, max_size: u32) -> Result<Self, StateStoreError> {
let cache = MokaCache::builder()
.max_capacity(max_size as u64)
.weigher(|_key: &ContractKey, value: &WrappedState| -> u32 { value.size() as u32 })
.build();
Ok(Self {
state_mem_cache: Some(cache),
store,
})
}
pub fn new_uncached(store: S) -> Self {
Self {
state_mem_cache: None,
store,
}
}
pub async fn update(
&mut self,
key: &ContractKey,
state: WrappedState,
) -> Result<(), StateStoreError> {
if state.size() > MAX_STATE_SIZE {
tracing::warn!(
contract = %key,
size_bytes = state.size(),
limit_bytes = MAX_STATE_SIZE,
"Rejecting oversized state at storage layer (update)"
);
return Err(StateStoreError::StateTooLarge {
key: *key,
size: state.size(),
limit: MAX_STATE_SIZE,
});
}
let cache_miss = if let Some(cache) = &self.state_mem_cache {
cache.get(key).is_none()
} else {
true
};
if cache_miss {
self.store
.get(key)
.await
.map_err(Into::into)?
.ok_or_else(|| StateStoreError::MissingContract(*key))?;
}
if let Some(cache) = &self.state_mem_cache {
cache.insert(*key, state.clone());
}
self.store.store(*key, state).await.map_err(Into::into)?;
Ok(())
}
pub async fn store(
&mut self,
key: ContractKey,
state: WrappedState,
params: Parameters<'static>,
) -> Result<(), StateStoreError> {
if state.size() > MAX_STATE_SIZE {
tracing::warn!(
contract = %key,
size_bytes = state.size(),
limit_bytes = MAX_STATE_SIZE,
"Rejecting oversized state at storage layer (store)"
);
return Err(StateStoreError::StateTooLarge {
key,
size: state.size(),
limit: MAX_STATE_SIZE,
});
}
if let Some(cache) = &self.state_mem_cache {
cache.insert(key, state.clone());
}
self.store.store(key, state).await.map_err(Into::into)?;
self.store
.store_params(key, params.clone())
.await
.map_err(Into::into)?;
Ok(())
}
pub async fn get(&self, key: &ContractKey) -> Result<WrappedState, StateStoreError> {
if let Some(cache) = &self.state_mem_cache {
if let Some(v) = cache.get(key) {
return Ok(v);
}
}
let r = self.store.get(key).await.map_err(Into::into)?;
r.ok_or_else(|| StateStoreError::MissingContract(*key))
}
pub async fn get_params<'a>(
&'a self,
key: &'a ContractKey,
) -> Result<Option<Parameters<'static>>, StateStoreError> {
let r = self.store.get_params(key).await.map_err(Into::into)?;
Ok(r)
}
pub async fn ensure_params(
&self,
key: ContractKey,
params: Parameters<'static>,
) -> Result<(), StateStoreError> {
self.store
.store_params(key, params)
.await
.map_err(|e| StateStoreError::Any(e.into()))?;
Ok(())
}
pub fn inner(&self) -> &S {
&self.store
}
pub fn storage(&self) -> S
where
S: Clone,
{
self.store.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wasm_runtime::mock_state_storage::MockStateStorage;
fn make_test_key() -> ContractKey {
let code = ContractCode::from(vec![1, 2, 3, 4]);
let params = Parameters::from(vec![5, 6, 7, 8]);
ContractKey::from_params_and_code(¶ms, &code)
}
fn make_test_key_with_code(code_bytes: &[u8]) -> ContractKey {
let code = ContractCode::from(code_bytes.to_vec());
let params = Parameters::from(vec![5, 6, 7, 8]);
ContractKey::from_params_and_code(¶ms, &code)
}
fn make_test_state(data: &[u8]) -> WrappedState {
WrappedState::new(data.to_vec())
}
#[tokio::test]
async fn test_state_store_basic_operations() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, state.clone(), params.clone())
.await
.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, state);
let retrieved_params = store.get_params(&key).await.unwrap();
assert_eq!(retrieved_params, Some(params));
}
#[tokio::test]
async fn test_state_store_get_nonexistent() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let result = store.get(&key).await;
assert!(matches!(result, Err(StateStoreError::MissingContract(_))));
}
#[tokio::test]
async fn test_state_store_update_nonexistent() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let result = store.update(&key, state).await;
assert!(matches!(result, Err(StateStoreError::MissingContract(_))));
}
#[tokio::test]
async fn test_state_store_update_existing() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let initial_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, initial_state, params).await.unwrap();
store.update(&key, updated_state.clone()).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, updated_state);
}
#[tokio::test]
async fn test_state_store_storage_failure_on_store() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_stores(1);
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_params_failure_on_store() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_store_params(1);
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_failure_on_update() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let initial_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, initial_state, params).await.unwrap();
mock_storage.fail_next_stores(1);
let result = store.update(&key, updated_state).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_failure_on_get() {
let mock_storage = MockStateStorage::new();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
mock_storage.seed_state(key, state);
mock_storage.fail_next_gets(1);
let store = StateStore::new(mock_storage, 10_000).unwrap();
let result = store.get(&key).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_state_store_cache_populated_on_store() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, state.clone(), params).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, state);
for _ in 0..5 {
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, state);
}
}
#[tokio::test]
async fn test_state_store_cache_update_coherence() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key = make_test_key();
let initial_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, initial_state, params).await.unwrap();
store.update(&key, updated_state.clone()).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, updated_state);
}
#[tokio::test]
async fn test_state_store_multiple_contracts() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key1 = make_test_key_with_code(&[1]);
let key2 = make_test_key_with_code(&[2]);
let key3 = make_test_key_with_code(&[3]);
let state1 = make_test_state(&[10]);
let state2 = make_test_state(&[20]);
let state3 = make_test_state(&[30]);
let params = Parameters::from(vec![5, 6, 7, 8]);
store
.store(key1, state1.clone(), params.clone())
.await
.unwrap();
store
.store(key2, state2.clone(), params.clone())
.await
.unwrap();
store.store(key3, state3.clone(), params).await.unwrap();
assert_eq!(store.get(&key1).await.unwrap(), state1);
assert_eq!(store.get(&key2).await.unwrap(), state2);
assert_eq!(store.get(&key3).await.unwrap(), state3);
}
#[tokio::test]
async fn test_state_store_isolated_failures() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage.clone(), 10_000).unwrap();
let key1 = make_test_key_with_code(&[1]);
let key2 = make_test_key_with_code(&[2]);
let state1 = make_test_state(&[10]);
let state2 = make_test_state(&[20]);
let params = Parameters::from(vec![5, 6, 7, 8]);
store
.store(key1, state1.clone(), params.clone())
.await
.unwrap();
mock_storage.fail_for_key(key2);
let result = store.store(key2, state2, params).await;
assert!(result.is_err());
assert_eq!(store.get(&key1).await.unwrap(), state1);
}
#[tokio::test]
async fn test_state_store_empty_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 10_000).unwrap();
let key = make_test_key();
let empty_state = make_test_state(&[]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, empty_state.clone(), params).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, empty_state);
assert_eq!(retrieved.size(), 0);
}
#[tokio::test]
async fn test_store_rejects_oversized_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let oversized = make_test_state(&vec![0u8; MAX_STATE_SIZE + 1]);
let params = Parameters::from(vec![1, 2, 3]);
let result = store.store(key, oversized, params).await;
assert!(
matches!(result, Err(StateStoreError::StateTooLarge { .. })),
"Expected StateTooLarge, got: {result:?}"
);
}
#[tokio::test]
async fn test_update_rejects_oversized_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let small = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![1, 2, 3]);
store.store(key, small, params).await.unwrap();
let oversized = make_test_state(&vec![0u8; MAX_STATE_SIZE + 1]);
let result = store.update(&key, oversized).await;
assert!(
matches!(result, Err(StateStoreError::StateTooLarge { .. })),
"Expected StateTooLarge, got: {result:?}"
);
}
#[tokio::test]
async fn test_store_accepts_state_at_limit() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let at_limit = make_test_state(&vec![0u8; MAX_STATE_SIZE]);
let params = Parameters::from(vec![1, 2, 3]);
store
.store(key, at_limit, params)
.await
.expect("State at exactly MAX_STATE_SIZE should be accepted");
}
#[tokio::test]
async fn test_state_store_large_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new(mock_storage, 2_000_000).unwrap();
let key = make_test_key();
let large_data: Vec<u8> = (0..1_000_000).map(|i| (i % 256) as u8).collect();
let large_state = make_test_state(&large_data);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, large_state.clone(), params).await.unwrap();
let retrieved = store.get(&key).await.unwrap();
assert_eq!(retrieved, large_state);
assert_eq!(retrieved.size(), 1_000_000);
}
#[tokio::test]
async fn test_uncached_store_failure_leaves_no_state() {
let mock_storage = MockStateStorage::new();
mock_storage.fail_next_stores(1);
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
let result = store.store(key, state, params).await;
assert!(result.is_err());
let get_result = store.get(&key).await;
assert!(
matches!(get_result, Err(StateStoreError::MissingContract(_))),
"Expected MissingContract after failed store, got {:?}",
get_result
);
}
#[tokio::test]
async fn test_uncached_update_failure_preserves_original_state() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage.clone());
let key = make_test_key();
let original_state = make_test_state(&[1, 2, 3]);
let updated_state = make_test_state(&[4, 5, 6]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, original_state.clone(), params)
.await
.unwrap();
mock_storage.fail_next_stores(1);
let result = store.update(&key, updated_state).await;
assert!(result.is_err());
let retrieved = store.get(&key).await.unwrap();
assert_eq!(
retrieved, original_state,
"Original state should be preserved after failed update"
);
}
#[tokio::test]
async fn test_uncached_sequential_updates_with_failures() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage.clone());
let key = make_test_key();
let state_v1 = make_test_state(&[1]);
let state_v2 = make_test_state(&[2]);
let state_v3 = make_test_state(&[3]);
let params = Parameters::from(vec![10, 20, 30]);
store.store(key, state_v1.clone(), params).await.unwrap();
store.update(&key, state_v2.clone()).await.unwrap();
assert_eq!(store.get(&key).await.unwrap(), state_v2);
mock_storage.fail_next_stores(1);
let result = store.update(&key, state_v3).await;
assert!(result.is_err());
let retrieved = store.get(&key).await.unwrap();
assert_eq!(
retrieved, state_v2,
"State should be v2 after v3 update failed"
);
}
#[tokio::test]
async fn test_ensure_params_persists_independently() {
let mock_storage = MockStateStorage::new();
let mut store = StateStore::new_uncached(mock_storage);
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
store
.store(key, state.clone(), params.clone())
.await
.unwrap();
assert_eq!(store.get_params(&key).await.unwrap(), Some(params.clone()));
store.ensure_params(key, params.clone()).await.unwrap();
assert_eq!(store.get_params(&key).await.unwrap(), Some(params));
}
#[tokio::test]
async fn test_ensure_params_fills_gap_when_params_missing() {
let mock_storage = MockStateStorage::new();
let store = StateStore::new_uncached(mock_storage.clone());
let key = make_test_key();
let state = make_test_state(&[1, 2, 3]);
let params = Parameters::from(vec![10, 20, 30]);
mock_storage.seed_state(key, state);
assert_eq!(store.get_params(&key).await.unwrap(), None);
store.ensure_params(key, params.clone()).await.unwrap();
assert_eq!(store.get_params(&key).await.unwrap(), Some(params));
}
}