use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DelegateApiVersion {
V1,
V2,
}
#[allow(dead_code)] impl DelegateApiVersion {
pub fn has_contract_host_functions(&self) -> bool {
matches!(self, DelegateApiVersion::V2)
}
}
impl fmt::Display for DelegateApiVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DelegateApiVersion::V1 => write!(f, "v1"),
DelegateApiVersion::V2 => write!(f, "v2"),
}
}
}
#[allow(dead_code)] pub mod contract_error_codes {
pub const SUCCESS: i32 = 0;
pub const ERR_NOT_IN_PROCESS: i32 = -1;
pub const ERR_CONTRACT_NOT_FOUND: i32 = -7;
pub const ERR_BUFFER_TOO_SMALL: i32 = -6;
pub const ERR_INVALID_PARAM: i32 = -4;
pub const ERR_STORE_ERROR: i32 = -8;
pub const ERR_MEMORY_BOUNDS: i32 = -9;
pub const ERR_CONTRACT_CODE_NOT_REGISTERED: i32 = -10;
}
#[allow(dead_code)] pub mod delegate_mgmt_error_codes {
pub const SUCCESS: i32 = 0;
pub const ERR_NOT_IN_PROCESS: i32 = -1;
pub const ERR_INVALID_PARAM: i32 = -4;
pub const ERR_MEMORY_BOUNDS: i32 = -9;
pub const ERR_DEPTH_EXCEEDED: i32 = -20;
pub const ERR_CREATIONS_EXCEEDED: i32 = -21;
pub const ERR_NODE_LIMIT_EXCEEDED: i32 = -22;
pub const ERR_INVALID_WASM: i32 = -23;
pub const ERR_STORE_FAILED: i32 = -24;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wasm_runtime::native_api::DelegateEnvError;
#[test]
fn test_version_display() {
assert_eq!(DelegateApiVersion::V1.to_string(), "v1");
assert_eq!(DelegateApiVersion::V2.to_string(), "v2");
}
#[test]
fn test_v1_no_contract_host_functions() {
assert!(!DelegateApiVersion::V1.has_contract_host_functions());
}
#[test]
fn test_v2_has_contract_host_functions() {
assert!(DelegateApiVersion::V2.has_contract_host_functions());
}
use crate::contract::storages::Storage;
use crate::util::tests::get_temp_dir;
use crate::wasm_runtime::StateStorage;
use freenet_stdlib::prelude::*;
fn make_contract_key(seed: u8) -> (ContractKey, ContractInstanceId, CodeHash) {
let code = ContractCode::from(vec![seed, seed + 1, seed + 2]);
let params = Parameters::from(vec![seed + 10, seed + 11]);
let key = ContractKey::from_params_and_code(¶ms, &code);
let id = *key.id();
let code_hash = *key.code_hash();
(key, id, code_hash)
}
#[tokio::test]
async fn test_redb_get_state_sync_matches_async() {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let (key, _, _) = make_contract_key(1);
let state_data = vec![10, 20, 30, 40, 50];
let state = WrappedState::new(state_data.clone());
db.store(key, state).await.unwrap();
let sync_result = db.get_state_sync(&key).unwrap();
assert!(sync_result.is_some(), "sync get should find stored state");
assert_eq!(
sync_result.unwrap().as_ref(),
&state_data,
"sync result should match stored data"
);
let async_result = db.get(&key).await.unwrap();
assert!(async_result.is_some());
assert_eq!(async_result.unwrap().as_ref(), &state_data);
}
#[tokio::test]
async fn test_redb_get_state_sync_missing() {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let (key, _, _) = make_contract_key(99);
let result = db.get_state_sync(&key).unwrap();
assert!(result.is_none(), "should return None for missing contract");
}
#[tokio::test]
async fn test_redb_get_state_sync_empty_state() {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let (key, _, _) = make_contract_key(2);
let state = WrappedState::new(vec![]);
db.store(key, state).await.unwrap();
let result = db.get_state_sync(&key).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().size(), 0, "empty state should have size 0");
}
#[tokio::test]
async fn test_redb_get_state_sync_after_update() {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let (key, _, _) = make_contract_key(3);
db.store(key, WrappedState::new(vec![1, 1, 1]))
.await
.unwrap();
db.store(key, WrappedState::new(vec![9, 9, 9]))
.await
.unwrap();
let result = db.get_state_sync(&key).unwrap().unwrap();
assert_eq!(result.as_ref(), &[9, 9, 9]);
}
use super::super::contract_store::ContractStore;
use super::super::native_api::DelegateCallEnv;
use super::super::secrets_store::SecretsStore;
struct TestEnv {
_temp_dir: tempfile::TempDir,
contract_store: ContractStore,
delegate_store: super::super::delegate_store::DelegateStore,
secret_store: SecretsStore,
db: Storage,
}
impl TestEnv {
async fn new() -> Self {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let contracts_dir = temp_dir.path().join("contracts");
let delegates_dir = temp_dir.path().join("delegates");
let secrets_dir = temp_dir.path().join("secrets");
let contract_store = ContractStore::new(contracts_dir, 10_000_000, db.clone()).unwrap();
let delegate_store =
super::super::delegate_store::DelegateStore::new(delegates_dir, 10_000, db.clone())
.unwrap();
let secret_store =
SecretsStore::new(secrets_dir, Default::default(), db.clone()).unwrap();
Self {
_temp_dir: temp_dir,
contract_store,
delegate_store,
secret_store,
db,
}
}
async fn store_contract(&mut self, seed: u8, state_data: &[u8]) -> ContractInstanceId {
let code = ContractCode::from(vec![seed, seed + 1, seed + 2]);
let params = Parameters::from(vec![seed + 10, seed + 11]);
let key = ContractKey::from_params_and_code(¶ms, &code);
let id = *key.id();
self.contract_store.ensure_key_indexed(&key).unwrap();
self.db
.store(key, WrappedState::new(state_data.to_vec()))
.await
.unwrap();
id
}
unsafe fn make_env(&mut self) -> DelegateCallEnv {
unsafe { self.make_env_with_depth(0) }
}
unsafe fn make_env_with_depth(&mut self, depth: u32) -> DelegateCallEnv {
let delegate_key = DelegateKey::new([0u8; 32], CodeHash::new([0u8; 32]));
unsafe {
DelegateCallEnv::new(
vec![],
&mut self.secret_store,
&self.contract_store,
Some(self.db.clone()),
delegate_key,
&mut self.delegate_store,
depth,
vec![],
)
}
}
unsafe fn make_env_with_attestations(
&mut self,
attestations: Vec<ContractInstanceId>,
) -> DelegateCallEnv {
let delegate_key = DelegateKey::new([1u8; 32], CodeHash::new([1u8; 32]));
unsafe {
DelegateCallEnv::new(
vec![],
&mut self.secret_store,
&self.contract_store,
Some(self.db.clone()),
delegate_key,
&mut self.delegate_store,
0,
attestations,
)
}
}
}
#[tokio::test]
async fn test_env_get_contract_state_found() {
let mut env_holder = TestEnv::new().await;
let state_data = vec![100, 200, 255];
let contract_id = env_holder.store_contract(50, &state_data).await;
let env = unsafe { env_holder.make_env() };
let result = env.get_contract_state_sync(&contract_id);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(state_data));
}
#[tokio::test]
async fn test_env_get_contract_state_not_found() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let missing_id = ContractInstanceId::new([77u8; 32]);
let result = env.get_contract_state_sync(&missing_id);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[tokio::test]
async fn test_env_get_contract_state_empty() {
let mut env_holder = TestEnv::new().await;
let contract_id = env_holder.store_contract(60, &[]).await;
let env = unsafe { env_holder.make_env() };
let result = env.get_contract_state_sync(&contract_id);
assert!(result.is_ok());
assert_eq!(result.unwrap(), Some(vec![]));
}
#[tokio::test]
async fn test_env_get_multiple_contracts() {
let mut env_holder = TestEnv::new().await;
let id1 = env_holder.store_contract(10, &[1, 1, 1]).await;
let id2 = env_holder.store_contract(20, &[2, 2, 2]).await;
let id3 = env_holder.store_contract(30, &[3, 3, 3]).await;
let env = unsafe { env_holder.make_env() };
assert_eq!(
env.get_contract_state_sync(&id1).unwrap(),
Some(vec![1, 1, 1])
);
assert_eq!(
env.get_contract_state_sync(&id2).unwrap(),
Some(vec![2, 2, 2])
);
assert_eq!(
env.get_contract_state_sync(&id3).unwrap(),
Some(vec![3, 3, 3])
);
}
#[tokio::test]
async fn test_env_get_contract_state_no_store() {
let mut env_holder = TestEnv::new().await;
let delegate_key = DelegateKey::new([0u8; 32], CodeHash::new([0u8; 32]));
let env = unsafe {
DelegateCallEnv::new(
vec![],
&mut env_holder.secret_store,
&env_holder.contract_store,
None, delegate_key,
&mut env_holder.delegate_store,
0,
vec![],
)
};
let result = env.get_contract_state_sync(&ContractInstanceId::new([1u8; 32]));
assert!(matches!(result, Err(DelegateEnvError::StoreNotConfigured)));
}
#[tokio::test]
async fn test_env_get_large_contract_state() {
let mut env_holder = TestEnv::new().await;
let large_state: Vec<u8> = (0..1_000_000u32).map(|i| (i % 256) as u8).collect();
let contract_id = env_holder.store_contract(70, &large_state).await;
let env = unsafe { env_holder.make_env() };
let result = env.get_contract_state_sync(&contract_id).unwrap().unwrap();
assert_eq!(result.len(), 1_000_000);
assert_eq!(result, large_state);
}
#[tokio::test]
async fn test_redb_store_state_sync_basic() {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let (key, _, _) = make_contract_key(40);
let state_data = vec![10, 20, 30, 40, 50];
db.store_state_sync(&key, WrappedState::new(state_data.clone()))
.unwrap();
let result = db.get_state_sync(&key).unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().as_ref(), &state_data);
}
#[tokio::test]
async fn test_redb_store_state_sync_overwrite() {
let temp_dir = get_temp_dir();
let db = Storage::new(temp_dir.path()).await.unwrap();
let (key, _, _) = make_contract_key(41);
db.store_state_sync(&key, WrappedState::new(vec![1, 1, 1]))
.unwrap();
db.store_state_sync(&key, WrappedState::new(vec![9, 9, 9]))
.unwrap();
let result = db.get_state_sync(&key).unwrap().unwrap();
assert_eq!(result.as_ref(), &[9, 9, 9]);
}
#[tokio::test]
async fn test_env_put_contract_state_no_store() {
let mut env_holder = TestEnv::new().await;
let delegate_key = DelegateKey::new([0u8; 32], CodeHash::new([0u8; 32]));
let env = unsafe {
DelegateCallEnv::new(
vec![],
&mut env_holder.secret_store,
&env_holder.contract_store,
None,
delegate_key,
&mut env_holder.delegate_store,
0,
vec![],
)
};
let result =
env.put_contract_state_sync(&ContractInstanceId::new([1u8; 32]), vec![1, 2, 3]);
assert!(matches!(result, Err(DelegateEnvError::StoreNotConfigured)));
}
#[tokio::test]
async fn test_env_update_contract_state_no_store() {
let mut env_holder = TestEnv::new().await;
let delegate_key = DelegateKey::new([0u8; 32], CodeHash::new([0u8; 32]));
let env = unsafe {
DelegateCallEnv::new(
vec![],
&mut env_holder.secret_store,
&env_holder.contract_store,
None,
delegate_key,
&mut env_holder.delegate_store,
0,
vec![],
)
};
let result =
env.update_contract_state_sync(&ContractInstanceId::new([1u8; 32]), vec![1, 2, 3]);
assert!(matches!(result, Err(DelegateEnvError::StoreNotConfigured)));
}
#[tokio::test]
async fn test_env_put_contract_state() {
let mut env_holder = TestEnv::new().await;
let contract_id = env_holder.store_contract(80, &[1, 2, 3]).await;
let env = unsafe { env_holder.make_env() };
let result = env.put_contract_state_sync(&contract_id, vec![4, 5, 6]);
assert!(result.is_ok(), "put should succeed: {:?}", result);
let state = env.get_contract_state_sync(&contract_id).unwrap();
assert_eq!(state, Some(vec![4, 5, 6]));
}
#[tokio::test]
async fn test_env_put_contract_state_unregistered() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let missing_id = ContractInstanceId::new([88u8; 32]);
let result = env.put_contract_state_sync(&missing_id, vec![1, 2, 3]);
assert!(matches!(
result,
Err(DelegateEnvError::ContractCodeNotRegistered)
));
}
#[tokio::test]
async fn test_env_update_contract_state() {
let mut env_holder = TestEnv::new().await;
let contract_id = env_holder.store_contract(81, &[10, 20, 30]).await;
let env = unsafe { env_holder.make_env() };
let result = env.update_contract_state_sync(&contract_id, vec![40, 50, 60]);
assert!(result.is_ok(), "update should succeed: {:?}", result);
let state = env.get_contract_state_sync(&contract_id).unwrap();
assert_eq!(state, Some(vec![40, 50, 60]));
}
#[tokio::test]
async fn test_env_update_contract_state_nonexistent() {
let mut env_holder = TestEnv::new().await;
let code = ContractCode::from(vec![82, 83, 84]);
let params = Parameters::from(vec![92, 93]);
let key = ContractKey::from_params_and_code(¶ms, &code);
let contract_id = *key.id();
env_holder.contract_store.ensure_key_indexed(&key).unwrap();
let env = unsafe { env_holder.make_env() };
let result = env.update_contract_state_sync(&contract_id, vec![1, 2, 3]);
assert!(matches!(result, Err(DelegateEnvError::NoExistingState)));
}
#[tokio::test]
async fn test_env_subscribe_known() {
let mut env_holder = TestEnv::new().await;
let contract_id = env_holder.store_contract(90, &[1]).await;
let env = unsafe { env_holder.make_env() };
let result = env.subscribe_contract_sync(&contract_id);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_env_subscribe_unknown() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let missing_id = ContractInstanceId::new([99u8; 32]);
let result = env.subscribe_contract_sync(&missing_id);
assert!(matches!(
result,
Err(DelegateEnvError::ContractCodeNotRegistered)
));
}
#[test]
#[cfg(feature = "wasmtime-backend")]
fn test_wasmtime_async_host_function_roundtrip() {
use wasmtime::*;
let wat = r#"
(module
(import "host" "async_get_value" (func $get_value (result i32)))
(func (export "compute") (result i32)
call $get_value
i32.const 1
i32.add))
"#;
let engine = Engine::default();
let module = Module::new(&engine, wat).unwrap();
let mut store = Store::new(&engine, ());
let mut linker = Linker::new(&engine);
linker
.func_wrap("host", "async_get_value", || -> i32 { 41 })
.unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap();
let compute = instance
.get_typed_func::<(), i32>(&mut store, "compute")
.unwrap();
let result = compute.call(&mut store, ()).unwrap();
assert_eq!(result, 42, "async_get_value(41) + 1 should be 42");
}
#[test]
#[cfg(feature = "wasmtime-backend")]
fn test_wasmtime_mixed_sync_async_host_functions() {
use wasmtime::*;
let wat = r#"
(module
(import "host" "sync_add" (func $sync_add (param i32 i32) (result i32)))
(import "host" "async_mul" (func $async_mul (param i32 i32) (result i32)))
(func (export "compute") (param i32 i32) (result i32)
;; sync_add(a, b) + async_mul(a, b)
local.get 0
local.get 1
call $sync_add
local.get 0
local.get 1
call $async_mul
i32.add))
"#;
let engine = Engine::default();
let module = Module::new(&engine, wat).unwrap();
let mut store = Store::new(&engine, ());
let mut linker = Linker::new(&engine);
linker
.func_wrap("host", "sync_add", |a: i32, b: i32| -> i32 { a + b })
.unwrap();
linker
.func_wrap("host", "async_mul", |a: i32, b: i32| -> i32 { a * b })
.unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap();
let compute = instance
.get_typed_func::<(i32, i32), i32>(&mut store, "compute")
.unwrap();
let result = compute.call(&mut store, (3, 4)).unwrap();
assert_eq!(result, 19);
}
#[test]
#[cfg(feature = "wasmtime-backend")]
fn test_wasmtime_store_reuse() {
use wasmtime::*;
let wat = r#"
(module
(import "host" "inc" (func $inc (param i32) (result i32)))
(func (export "call_inc") (param i32) (result i32)
local.get 0
call $inc))
"#;
let engine = Engine::default();
let module = Module::new(&engine, wat).unwrap();
let mut store = Store::new(&engine, ());
let mut linker = Linker::new(&engine);
linker
.func_wrap("host", "inc", |x: i32| -> i32 { x + 1 })
.unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap();
let call_inc = instance
.get_typed_func::<i32, i32>(&mut store, "call_inc")
.unwrap();
let result1 = call_inc.call(&mut store, 10).unwrap();
assert_eq!(result1, 11);
let result2 = call_inc.call(&mut store, 20).unwrap();
assert_eq!(result2, 21);
let result3 = call_inc.call(&mut store, 30).unwrap();
assert_eq!(result3, 31);
}
use super::super::native_api::DelegateCreateError;
use crate::contract::{MAX_DELEGATE_CREATION_DEPTH, MAX_DELEGATE_CREATIONS_PER_CALL};
fn minimal_delegate_wasm() -> Vec<u8> {
b"\0asm\x01\x00\x00\x00".to_vec() }
#[tokio::test]
async fn test_create_delegate_depth_exceeded() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env_with_depth(MAX_DELEGATE_CREATION_DEPTH) };
let wasm = minimal_delegate_wasm();
let cipher = [0u8; 32];
let nonce = [0u8; 24];
let result = env.create_delegate_sync(&wasm, &[], cipher, nonce);
assert!(
matches!(result, Err(DelegateCreateError::DepthExceeded)),
"should reject at depth {}: {:?}",
MAX_DELEGATE_CREATION_DEPTH,
result
);
}
#[tokio::test]
async fn test_create_delegate_depth_just_under_limit() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env_with_depth(MAX_DELEGATE_CREATION_DEPTH - 1) };
let wasm = minimal_delegate_wasm();
let cipher = [0u8; 32];
let nonce = [0u8; 24];
let result = env.create_delegate_sync(&wasm, &[], cipher, nonce);
assert!(
result.is_ok(),
"should allow at depth {}: {:?}",
MAX_DELEGATE_CREATION_DEPTH - 1,
result
);
}
#[tokio::test]
async fn test_create_delegate_per_call_limit_exceeded() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let wasm = minimal_delegate_wasm();
let cipher = [0u8; 32];
let nonce = [0u8; 24];
for i in 0..MAX_DELEGATE_CREATIONS_PER_CALL {
let result = env.create_delegate_sync(&wasm, &[i as u8], cipher, nonce);
assert!(result.is_ok(), "creation {i} should succeed: {:?}", result);
}
let result = env.create_delegate_sync(&wasm, &[255], cipher, nonce);
assert!(
matches!(result, Err(DelegateCreateError::CreationsExceeded)),
"should reject at creation count {}: {:?}",
MAX_DELEGATE_CREATIONS_PER_CALL,
result
);
}
#[tokio::test]
async fn test_create_delegate_accepts_any_bytes_at_creation() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let arbitrary_bytes = vec![0xFF, 0xFF, 0xFF]; let cipher = [0u8; 32];
let nonce = [0u8; 24];
let result = env.create_delegate_sync(&arbitrary_bytes, &[], cipher, nonce);
assert!(
result.is_ok(),
"creation should accept any bytes (validation deferred to execution): {:?}",
result
);
}
#[tokio::test]
async fn test_create_delegate_rejects_oversized_wasm() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let oversized = vec![0u8; DelegateCallEnv::MAX_WASM_CODE_SIZE + 1];
let cipher = [0u8; 32];
let nonce = [0u8; 24];
let result = env.create_delegate_sync(&oversized, &[], cipher, nonce);
assert!(
matches!(result, Err(DelegateCreateError::InvalidWasm(_))),
"should reject oversized WASM: {:?}",
result
);
}
#[tokio::test]
async fn test_create_delegate_counter_tracks_correctly() {
let mut env_holder = TestEnv::new().await;
let env = unsafe { env_holder.make_env() };
let wasm = minimal_delegate_wasm();
let cipher = [0u8; 32];
let nonce = [0u8; 24];
assert_eq!(env.creations_this_call.get(), 0);
env.create_delegate_sync(&wasm, &[1], cipher, nonce)
.unwrap();
assert_eq!(env.creations_this_call.get(), 1);
env.create_delegate_sync(&wasm, &[2], cipher, nonce)
.unwrap();
assert_eq!(env.creations_this_call.get(), 2);
}
#[tokio::test]
async fn test_create_delegate_inherits_attestations() {
use super::super::native_api::DELEGATE_INHERITED_ORIGINS;
let mut env_holder = TestEnv::new().await;
let contract_id = ContractInstanceId::new([42u8; 32]);
let env = unsafe { env_holder.make_env_with_attestations(vec![contract_id]) };
let wasm = minimal_delegate_wasm();
let cipher = [0u8; 32];
let nonce = [0u8; 24];
let child_key = env
.create_delegate_sync(&wasm, &[1], cipher, nonce)
.unwrap();
let inherited = DELEGATE_INHERITED_ORIGINS.get(&child_key);
assert!(
inherited.is_some(),
"child should have inherited attestations"
);
assert_eq!(inherited.unwrap().value(), &vec![contract_id]);
DELEGATE_INHERITED_ORIGINS.remove(&child_key);
}
#[tokio::test]
async fn test_create_delegate_non_attested_still_counts_toward_node_limit() {
use super::super::native_api::{CREATED_DELEGATES_COUNT, DELEGATE_INHERITED_ORIGINS};
use std::sync::atomic::Ordering;
let mut env_holder = TestEnv::new().await;
let before = CREATED_DELEGATES_COUNT.load(Ordering::Relaxed);
let env = unsafe { env_holder.make_env() }; let wasm = minimal_delegate_wasm();
let cipher = [0u8; 32];
let nonce = [0u8; 24];
let child_key = env
.create_delegate_sync(&wasm, &[1], cipher, nonce)
.unwrap();
assert!(
DELEGATE_INHERITED_ORIGINS.get(&child_key).is_none(),
"non-attested parent should not create attestation entry"
);
let after = CREATED_DELEGATES_COUNT.load(Ordering::Relaxed);
assert!(
after > before,
"global counter should increment for all creations (before={before}, after={after})"
);
CREATED_DELEGATES_COUNT.fetch_sub(1, Ordering::Relaxed);
}
}