use std::collections::VecDeque;
use chacha20poly1305::{XChaCha20Poly1305, XNonce};
use freenet_stdlib::prelude::{
ApplicationMessage, DelegateContainer, DelegateContext, DelegateError, DelegateInterfaceResult,
DelegateKey, DelegateMessage, GetContractRequest, InboundDelegateMsg, MessageOrigin,
OutboundDelegateMsg, Parameters, PutContractRequest, SecretsId, SubscribeContractRequest,
UpdateContractRequest,
};
use super::engine::{InstanceHandle, WasmEngine};
use super::native_api::{CURRENT_DELEGATE_INSTANCE, DELEGATE_ENV, DelegateCallEnv, InstanceId};
use super::{Runtime, RuntimeResult};
use crate::wasm_runtime::delegate_api::DelegateApiVersion;
struct DelegateEnvGuard {
instance_id: InstanceId,
}
impl DelegateEnvGuard {
fn new(instance_id: InstanceId) -> Self {
Self { instance_id }
}
}
impl Drop for DelegateEnvGuard {
fn drop(&mut self) {
CURRENT_DELEGATE_INSTANCE.with(|c| c.set(-1));
DELEGATE_ENV.remove(&self.instance_id);
}
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DelegateExecError {
#[error(transparent)]
DelegateError(#[from] DelegateError),
#[error("Permission denied: secret {secret} cannot be accesed by {delegate} at this time")]
UnauthorizedSecretAccess {
secret: SecretsId,
delegate: DelegateKey,
},
#[error("Received an unexpected message from the client apps: {0}")]
UnexpectedMessage(&'static str),
}
pub(crate) trait DelegateRuntimeInterface {
fn inbound_app_message(
&mut self,
key: &DelegateKey,
params: &Parameters,
origin: Option<&MessageOrigin>,
inbound: Vec<InboundDelegateMsg>,
) -> RuntimeResult<Vec<OutboundDelegateMsg>>;
fn register_delegate(
&mut self,
delegate: DelegateContainer,
cipher: XChaCha20Poly1305,
nonce: XNonce,
) -> RuntimeResult<()>;
fn unregister_delegate(&mut self, key: &DelegateKey) -> RuntimeResult<()>;
}
impl Runtime {
#[allow(clippy::too_many_arguments)]
fn exec_inbound_with_env(
&mut self,
delegate_key: &DelegateKey,
params: &Parameters<'_>,
origin: Option<&MessageOrigin>,
msg: &InboundDelegateMsg,
context: Vec<u8>,
handle: &InstanceHandle,
instance_id: i64,
api_version: DelegateApiVersion,
) -> RuntimeResult<(Vec<OutboundDelegateMsg>, Vec<u8>)> {
let origin_contracts = match origin {
Some(MessageOrigin::WebApp(contract_id)) => vec![*contract_id],
Some(MessageOrigin::Delegate(_)) | None => Vec::new(),
Some(other) => {
tracing::warn!(
delegate_key = %delegate_key,
origin = ?other,
"Unknown MessageOrigin variant reached fail-closed default; \
wasm_runtime::delegate::Runtime::inbound_app_message must \
decide explicitly whether this variant grants contract access"
);
Vec::new()
}
};
let env = unsafe {
DelegateCallEnv::new(
context,
&mut self.secret_store,
&self.contract_store,
self.state_store_db.clone(),
delegate_key.clone(),
&mut self.delegate_store,
0, origin_contracts,
)
};
debug_assert!(
!DELEGATE_ENV.contains_key(&instance_id),
"Instance ID {instance_id} already exists in DELEGATE_ENV - this indicates a bug"
);
DELEGATE_ENV.insert(instance_id, env);
CURRENT_DELEGATE_INSTANCE.with(|c| c.set(instance_id));
let _guard = DelegateEnvGuard::new(instance_id);
let result = self.exec_inbound(params, origin, msg, handle, api_version);
let updated_context = DELEGATE_ENV
.get(&instance_id)
.map(|env| env.context.clone())
.unwrap_or_default();
let outbound = result?;
Ok((outbound, updated_context))
}
fn exec_inbound(
&mut self,
params: &Parameters<'_>,
origin: Option<&MessageOrigin>,
msg: &InboundDelegateMsg,
handle: &InstanceHandle,
api_version: DelegateApiVersion,
) -> RuntimeResult<Vec<OutboundDelegateMsg>> {
let param_buf_ptr = {
let mut param_buf = self.init_buf(handle, params)?;
param_buf.write(params)?;
param_buf.ptr()
};
let origin_buf_ptr = {
let bytes = match origin {
Some(o) => bincode::serialize(o)?,
None => Vec::new(),
};
let mut origin_buf = self.init_buf(handle, &bytes)?;
origin_buf.write(bytes)?;
origin_buf.ptr()
};
let msg_ptr = {
let msg = bincode::serialize(msg)?;
let mut msg_buf = self.init_buf(handle, &msg)?;
msg_buf.write(msg)?;
msg_buf.ptr()
};
let inbound_msg_name = match msg {
InboundDelegateMsg::ApplicationMessage(_) => "ApplicationMessage",
InboundDelegateMsg::UserResponse(_) => "UserResponse",
InboundDelegateMsg::GetContractResponse(_) => "GetContractResponse",
InboundDelegateMsg::PutContractResponse(_) => "PutContractResponse",
InboundDelegateMsg::UpdateContractResponse(_) => "UpdateContractResponse",
InboundDelegateMsg::SubscribeContractResponse(_) => "SubscribeContractResponse",
InboundDelegateMsg::ContractNotification(_) => "ContractNotification",
InboundDelegateMsg::DelegateMessage(_) => "DelegateMessage",
_ => "Unknown",
};
tracing::debug!(
inbound_msg_name,
api_version = %api_version,
"Calling delegate with inbound message"
);
let res = match api_version {
DelegateApiVersion::V1 => {
self.engine.call_3i64(
handle,
"process",
param_buf_ptr as i64,
origin_buf_ptr as i64,
msg_ptr as i64,
)?
}
DelegateApiVersion::V2 => {
self.engine.call_3i64_async_imports(
handle,
"process",
param_buf_ptr as i64,
origin_buf_ptr as i64,
msg_ptr as i64,
)?
}
};
let linear_mem = self.linear_mem(handle)?;
let outbound = unsafe {
DelegateInterfaceResult::from_raw(res, &linear_mem)
.unwrap(linear_mem)
.map_err(Into::<DelegateExecError>::into)?
};
self.log_delegate_exec_result(inbound_msg_name, &outbound);
Ok(outbound)
}
fn log_delegate_exec_result(&self, inbound_msg_name: &str, outbound: &[OutboundDelegateMsg]) {
if tracing::enabled!(tracing::Level::DEBUG) {
let outbound_message_names = outbound
.iter()
.map(|m| match m {
OutboundDelegateMsg::ApplicationMessage(am) => format!(
"ApplicationMessage(payload_len={}, processed={}, context_len={})",
am.payload.len(),
am.processed,
am.context.as_ref().len()
),
OutboundDelegateMsg::RequestUserInput(_) => "RequestUserInput".to_string(),
OutboundDelegateMsg::ContextUpdated(_) => "ContextUpdated".to_string(),
OutboundDelegateMsg::GetContractRequest(req) => {
format!("GetContractRequest(contract={})", req.contract_id)
}
OutboundDelegateMsg::PutContractRequest(req) => {
format!("PutContractRequest(contract={})", req.contract.key())
}
OutboundDelegateMsg::UpdateContractRequest(req) => {
format!("UpdateContractRequest(contract={})", req.contract_id)
}
OutboundDelegateMsg::SubscribeContractRequest(req) => {
format!("SubscribeContractRequest(contract={})", req.contract_id)
}
OutboundDelegateMsg::SendDelegateMessage(msg) => {
format!(
"SendDelegateMessage(target={}, payload_len={})",
msg.target,
msg.payload.len()
)
}
})
.collect::<Vec<String>>()
.join(", ");
tracing::debug!(
inbound_msg_name,
outbound_message_names,
"Delegate returned outbound messages"
);
} else {
tracing::debug!(
inbound_msg_name,
outbound_len = outbound.len(),
"Delegate returned outbound messages"
);
}
}
fn log_process_outbound_entry(
&self,
delegate_key: &DelegateKey,
origin: Option<&MessageOrigin>,
outbound_msgs: &VecDeque<OutboundDelegateMsg>,
) {
tracing::debug!(
delegate_key = ?delegate_key,
?origin,
outbound_msgs_len = outbound_msgs.len(),
outbound_msg_details = debug(if tracing::enabled!(tracing::Level::DEBUG) {
outbound_msgs.iter().map(|msg| {
match msg {
OutboundDelegateMsg::ApplicationMessage(m) => format!("AppMsg(payload_len={})", m.payload.len()),
OutboundDelegateMsg::RequestUserInput(_) => "UserInputReq".to_string(),
OutboundDelegateMsg::ContextUpdated(_) => "ContextUpdate".to_string(),
OutboundDelegateMsg::GetContractRequest(r) => format!("GetContractReq({})", r.contract_id),
OutboundDelegateMsg::PutContractRequest(r) => format!("PutContractReq({})", r.contract.key()),
OutboundDelegateMsg::UpdateContractRequest(r) => format!("UpdateContractReq({})", r.contract_id),
OutboundDelegateMsg::SubscribeContractRequest(r) => format!("SubscribeContractReq({})", r.contract_id),
OutboundDelegateMsg::SendDelegateMessage(m) => format!("SendDelegateMsg(target={})", m.target),
}
}).collect::<Vec<_>>()
} else {
Vec::new()
}),
"process_outbound called"
);
}
#[allow(clippy::too_many_arguments)]
fn process_outbound(
&mut self,
delegate_key: &DelegateKey,
_handle: &InstanceHandle,
_instance_id: i64,
_params: &Parameters<'_>,
origin: Option<&MessageOrigin>,
outbound_msgs: &mut VecDeque<OutboundDelegateMsg>,
context: &mut Vec<u8>,
results: &mut Vec<OutboundDelegateMsg>,
) -> RuntimeResult<()> {
self.log_process_outbound_entry(delegate_key, origin, outbound_msgs);
while let Some(outbound) = outbound_msgs.pop_front() {
match outbound {
OutboundDelegateMsg::ApplicationMessage(mut msg) => {
tracing::debug!(
payload_len = msg.payload.len(),
processed = msg.processed,
"Adding ApplicationMessage to results"
);
msg.context = DelegateContext::default();
results.push(OutboundDelegateMsg::ApplicationMessage(msg));
for remaining in outbound_msgs.drain(..) {
results.push(remaining);
}
break;
}
OutboundDelegateMsg::RequestUserInput(req) => {
tracing::debug!(
request_id = req.request_id,
"Passing RequestUserInput to executor for user prompting"
);
results.push(OutboundDelegateMsg::RequestUserInput(req));
for remaining in outbound_msgs.drain(..) {
results.push(remaining);
}
break;
}
OutboundDelegateMsg::ContextUpdated(new_context) => {
*context = new_context.as_ref().to_vec();
}
OutboundDelegateMsg::GetContractRequest(req) if !req.processed => {
tracing::debug!(
contract_id = %req.contract_id,
"Passing GetContractRequest to executor for async handling"
);
results.push(OutboundDelegateMsg::GetContractRequest(req));
for remaining in outbound_msgs.drain(..) {
results.push(remaining);
}
break;
}
OutboundDelegateMsg::GetContractRequest(GetContractRequest {
context: ctx,
..
}) => {
tracing::debug!("GetContractRequest processed");
*context = ctx.as_ref().to_vec();
}
OutboundDelegateMsg::PutContractRequest(req) if !req.processed => {
tracing::debug!(
contract = %req.contract.key(),
"Passing PutContractRequest to executor for async handling"
);
results.push(OutboundDelegateMsg::PutContractRequest(req));
for remaining in outbound_msgs.drain(..) {
results.push(remaining);
}
break;
}
OutboundDelegateMsg::PutContractRequest(PutContractRequest {
context: ctx,
..
}) => {
tracing::debug!("PutContractRequest processed");
*context = ctx.as_ref().to_vec();
}
OutboundDelegateMsg::UpdateContractRequest(req) if !req.processed => {
tracing::debug!(
contract_id = %req.contract_id,
"Passing UpdateContractRequest to executor for async handling"
);
results.push(OutboundDelegateMsg::UpdateContractRequest(req));
for remaining in outbound_msgs.drain(..) {
results.push(remaining);
}
break;
}
OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
context: ctx,
..
}) => {
tracing::debug!("UpdateContractRequest processed");
*context = ctx.as_ref().to_vec();
}
OutboundDelegateMsg::SubscribeContractRequest(req) if !req.processed => {
tracing::debug!(
contract_id = %req.contract_id,
"Passing SubscribeContractRequest to executor for async handling"
);
results.push(OutboundDelegateMsg::SubscribeContractRequest(req));
for remaining in outbound_msgs.drain(..) {
results.push(remaining);
}
break;
}
OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
context: ctx,
..
}) => {
tracing::debug!("SubscribeContractRequest processed");
*context = ctx.as_ref().to_vec();
}
OutboundDelegateMsg::SendDelegateMessage(mut msg) if !msg.processed => {
tracing::debug!(
target_delegate = %msg.target,
"Passing SendDelegateMessage to executor for delivery"
);
msg.sender = delegate_key.clone();
results.push(OutboundDelegateMsg::SendDelegateMessage(msg));
for remaining in outbound_msgs.drain(..) {
match remaining {
OutboundDelegateMsg::SendDelegateMessage(mut m) if !m.processed => {
m.sender = delegate_key.clone();
results.push(OutboundDelegateMsg::SendDelegateMessage(m));
}
msg @ (OutboundDelegateMsg::ApplicationMessage(_)
| OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_)) => results.push(msg),
}
}
break;
}
OutboundDelegateMsg::SendDelegateMessage(DelegateMessage {
context: ctx, ..
}) => {
tracing::debug!("SendDelegateMessage processed");
*context = ctx.as_ref().to_vec();
}
}
}
Ok(())
}
}
impl DelegateRuntimeInterface for Runtime {
fn inbound_app_message(
&mut self,
delegate_key: &DelegateKey,
params: &Parameters,
origin: Option<&MessageOrigin>,
inbound: Vec<InboundDelegateMsg>,
) -> RuntimeResult<Vec<OutboundDelegateMsg>> {
let mut results = Vec::with_capacity(inbound.len());
if inbound.is_empty() {
return Ok(results);
}
let (mut running, api_version) = self.prepare_delegate_call(params, delegate_key, 4096)?;
let instance_id = running.id;
tracing::debug!(
delegate_key = %delegate_key,
api_version = %api_version,
"Starting delegate execution"
);
let mut context: Vec<u8> = Vec::new();
let process_result: RuntimeResult<()> = (|| {
for msg in inbound {
#[allow(clippy::wildcard_enum_match_arm)]
match msg {
InboundDelegateMsg::ApplicationMessage(ApplicationMessage {
payload,
processed,
..
}) => {
let app_msg = InboundDelegateMsg::ApplicationMessage(
ApplicationMessage::new(payload)
.processed(processed)
.with_context(DelegateContext::new(context.clone())),
);
let (outbound, updated_context) = self.exec_inbound_with_env(
delegate_key,
params,
origin,
&app_msg,
context.clone(),
&running.handle,
instance_id,
api_version,
)?;
context = updated_context;
let mut outbound_queue = VecDeque::from(outbound);
self.process_outbound(
delegate_key,
&running.handle,
instance_id,
params,
origin,
&mut outbound_queue,
&mut context,
&mut results,
)?;
}
InboundDelegateMsg::UserResponse(response) => {
let (outbound, updated_context) = self.exec_inbound_with_env(
delegate_key,
params,
origin,
&InboundDelegateMsg::UserResponse(response),
context.clone(),
&running.handle,
instance_id,
api_version,
)?;
context = updated_context;
let mut outbound_queue = VecDeque::from(outbound);
self.process_outbound(
delegate_key,
&running.handle,
instance_id,
params,
origin,
&mut outbound_queue,
&mut context,
&mut results,
)?;
}
InboundDelegateMsg::GetContractResponse(response) => {
let (outbound, updated_context) = self.exec_inbound_with_env(
delegate_key,
params,
origin,
&InboundDelegateMsg::GetContractResponse(response),
context.clone(),
&running.handle,
instance_id,
api_version,
)?;
context = updated_context;
let mut outbound_queue = VecDeque::from(outbound);
self.process_outbound(
delegate_key,
&running.handle,
instance_id,
params,
origin,
&mut outbound_queue,
&mut context,
&mut results,
)?;
}
msg @ (InboundDelegateMsg::PutContractResponse(_)
| InboundDelegateMsg::UpdateContractResponse(_)
| InboundDelegateMsg::SubscribeContractResponse(_)
| InboundDelegateMsg::ContractNotification(_)
| InboundDelegateMsg::DelegateMessage(_)) => {
let (outbound, updated_context) = self.exec_inbound_with_env(
delegate_key,
params,
origin,
&msg,
context.clone(),
&running.handle,
instance_id,
api_version,
)?;
context = updated_context;
let mut outbound_queue = VecDeque::from(outbound);
self.process_outbound(
delegate_key,
&running.handle,
instance_id,
params,
origin,
&mut outbound_queue,
&mut context,
&mut results,
)?;
}
other => {
let (outbound, updated_context) = self.exec_inbound_with_env(
delegate_key,
params,
origin,
&other,
context.clone(),
&running.handle,
instance_id,
api_version,
)?;
context = updated_context;
let mut outbound_queue = VecDeque::from(outbound);
self.process_outbound(
delegate_key,
&running.handle,
instance_id,
params,
origin,
&mut outbound_queue,
&mut context,
&mut results,
)?;
}
}
}
Ok(())
})();
self.drop_running_instance(&mut running);
process_result?;
tracing::debug!(
count = results.len(),
"Final results returned by inbound_app_message"
);
Ok(results)
}
#[inline]
fn register_delegate(
&mut self,
delegate: DelegateContainer,
cipher: XChaCha20Poly1305,
nonce: XNonce,
) -> RuntimeResult<()> {
self.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce)?;
self.delegate_store.store_delegate(delegate)
}
#[inline]
fn unregister_delegate(&mut self, key: &DelegateKey) -> RuntimeResult<()> {
self.delegate_modules.lock().unwrap().pop(key);
self.delegate_store.remove_delegate(key)
}
}
#[cfg(all(test, feature = "wasmtime-backend"))]
mod test {
use chacha20poly1305::aead::{AeadCore, KeyInit, OsRng};
use freenet_stdlib::prelude::*;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::os::unix::fs::PermissionsExt;
use crate::util::tests::get_temp_dir;
use super::super::{ContractStore, SecretsStore, delegate_store::DelegateStore};
use super::*;
const TEST_DELEGATE_2: &str = "test_delegate_2";
mod delegate2_messages {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
pub enum InboundAppMessage {
CreateInboxRequest,
PleaseSignMessage(Vec<u8>),
WriteContext(Vec<u8>),
ReadContext,
ClearContext,
IncrementCounter,
HasSecret(Vec<u8>),
GetNonExistentSecret(Vec<u8>),
StoreSecret { key: Vec<u8>, value: Vec<u8> },
RemoveSecret(Vec<u8>),
WriteLargeContext(usize),
StoreLargeSecret { key: Vec<u8>, size: usize },
}
#[derive(Debug, Serialize, Deserialize)]
pub enum OutboundAppMessage {
CreateInboxResponse(Vec<u8>),
MessageSigned(Vec<u8>),
ContextData(Vec<u8>),
CounterValue(u32),
SecretExists(bool),
SecretResult(Option<Vec<u8>>),
ContextWritten,
ContextCleared,
SecretStored,
SecretRemoved,
LargeContextWritten(usize),
LargeSecretStored(usize),
SecretStoreFailed,
}
}
async fn setup_runtime(
name: &str,
) -> Result<(DelegateContainer, Runtime, tempfile::TempDir), Box<dyn std::error::Error>> {
use crate::contract::storages::Storage;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db)?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
let delegate = {
let bytes = super::super::tests::get_test_module(name)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
&vec![].into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
Ok((delegate, runtime, temp_dir))
}
const TEST_DELEGATE_CAPABILITIES: &str = "test_delegate_capabilities";
mod capabilities_messages {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
#[allow(clippy::enum_variant_names)]
pub enum DelegateCommand {
GetContractState {
contract_id: ContractInstanceId,
},
GetMultipleContractStates {
contract_ids: Vec<ContractInstanceId>,
},
GetContractWithEcho {
contract_id: ContractInstanceId,
echo_message: String,
},
PutContractState {
contract: ContractContainer,
state: Vec<u8>,
},
UpdateContractState {
contract_id: ContractInstanceId,
state: Vec<u8>,
},
SubscribeContract {
contract_id: ContractInstanceId,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub enum DelegateResponse {
ContractState {
contract_id: ContractInstanceId,
state: Option<Vec<u8>>,
},
MultipleContractStates {
results: Vec<(ContractInstanceId, Option<Vec<u8>>)>,
},
Echo {
message: String,
},
ContractPutResult {
contract_id: ContractInstanceId,
success: bool,
error: Option<String>,
},
ContractUpdateResult {
contract_id: ContractInstanceId,
success: bool,
error: Option<String>,
},
ContractSubscribeResult {
contract_id: ContractInstanceId,
success: bool,
error: Option<String>,
},
ContractNotificationReceived {
contract_id: ContractInstanceId,
new_state: Vec<u8>,
},
Error {
message: String,
},
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_contract_request_response() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let target_contract_id = ContractInstanceId::new([42u8; 32]);
let _app_id = ContractInstanceId::new([1u8; 32]);
let command = DelegateCommand::GetContractState {
contract_id: target_contract_id,
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let contract_request = match &outbound[0] {
OutboundDelegateMsg::GetContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected GetContractRequest, got {:?}", other)
}
};
assert_eq!(contract_request.contract_id, target_contract_id);
assert!(!contract_request.processed);
let contract_state = vec![1, 2, 3, 4, 5];
let response = InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id: target_contract_id,
state: Some(WrappedState::new(contract_state.clone())),
context: contract_request.context.clone(),
});
let final_outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response])?;
assert_eq!(final_outbound.len(), 1);
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(final_msg.processed);
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::ContractState { contract_id, state } => {
assert_eq!(contract_id, target_contract_id);
assert_eq!(state, Some(contract_state));
}
other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractState response, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_contract_not_found() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let target_contract_id = ContractInstanceId::new([99u8; 32]);
let _app_id = ContractInstanceId::new([1u8; 32]);
let command = DelegateCommand::GetContractState {
contract_id: target_contract_id,
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
let contract_request = match &outbound[0] {
OutboundDelegateMsg::GetContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected GetContractRequest, got {:?}", other)
}
};
let response = InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id: target_contract_id,
state: None,
context: contract_request.context.clone(),
});
let final_outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response])?;
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::ContractState { state, .. } => {
assert!(state.is_none());
}
other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractState response, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multiple_contract_requests() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let contract1 = ContractInstanceId::new([1u8; 32]);
let contract2 = ContractInstanceId::new([2u8; 32]);
let contract3 = ContractInstanceId::new([3u8; 32]);
let _app_id = ContractInstanceId::new([10u8; 32]);
let command = DelegateCommand::GetMultipleContractStates {
contract_ids: vec![contract1, contract2, contract3],
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1);
let req1 = match &outbound[0] {
OutboundDelegateMsg::GetContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected GetContractRequest, got {:?}", other)
}
};
assert_eq!(req1.contract_id, contract1);
let response1 = InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id: contract1,
state: Some(WrappedState::new(vec![1, 1, 1])),
context: req1.context,
});
let outbound2 =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response1])?;
assert_eq!(outbound2.len(), 1);
let req2 = match &outbound2[0] {
OutboundDelegateMsg::GetContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected GetContractRequest for contract2, got {:?}", other)
}
};
assert_eq!(req2.contract_id, contract2);
let response2 = InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id: contract2,
state: Some(WrappedState::new(vec![2, 2, 2])),
context: req2.context,
});
let outbound3 =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response2])?;
assert_eq!(outbound3.len(), 1);
let req3 = match &outbound3[0] {
OutboundDelegateMsg::GetContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected GetContractRequest for contract3, got {:?}", other)
}
};
assert_eq!(req3.contract_id, contract3);
let response3 = InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id: contract3,
state: Some(WrappedState::new(vec![3, 3, 3])),
context: req3.context,
});
let final_outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response3])?;
assert_eq!(final_outbound.len(), 1);
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(final_msg.processed);
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::MultipleContractStates { results } => {
assert_eq!(results.len(), 3);
assert_eq!(results[0].0, contract1);
assert_eq!(results[0].1, Some(vec![1, 1, 1]));
assert_eq!(results[1].0, contract2);
assert_eq!(results[1].1, Some(vec![2, 2, 2]));
assert_eq!(results[2].0, contract3);
assert_eq!(results[2].1, Some(vec![3, 3, 3]));
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected MultipleContractStates, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_message_accumulation() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let contract_id = ContractInstanceId::new([42u8; 32]);
let _app_id = ContractInstanceId::new([1u8; 32]);
let echo_message = "Hello from test!".to_string();
let command = DelegateCommand::GetContractWithEcho {
contract_id,
echo_message: echo_message.clone(),
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 2);
let contract_request = outbound
.iter()
.find_map(|msg| match msg {
OutboundDelegateMsg::GetContractRequest(req) => Some(req.clone()),
OutboundDelegateMsg::ApplicationMessage(_)
| OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => None,
})
.expect("Expected a GetContractRequest");
assert_eq!(contract_request.contract_id, contract_id);
let echo_msg = outbound
.iter()
.find_map(|msg| match msg {
OutboundDelegateMsg::ApplicationMessage(m) => Some(m.clone()),
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => None,
})
.expect("Expected an ApplicationMessage (Echo)");
assert!(echo_msg.processed);
let echo_response: DelegateResponse = bincode::deserialize(&echo_msg.payload)?;
match echo_response {
DelegateResponse::Echo { message } => {
assert_eq!(message, echo_message);
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected Echo response, got {:?}", other)
}
}
let contract_response = InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id,
state: Some(WrappedState::new(vec![1, 2, 3, 4])),
context: contract_request.context,
});
let final_outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![contract_response],
)?;
assert_eq!(final_outbound.len(), 1);
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::ContractState {
contract_id: id,
state,
} => {
assert_eq!(id, contract_id);
assert_eq!(state, Some(vec![1, 2, 3, 4]));
}
other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractState response, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn validate_host_function_delegate() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let payload: Vec<u8> = bincode::serialize(&InboundAppMessage::CreateInboxRequest).unwrap();
let create_msg = ApplicationMessage::new(payload);
let inbound = InboundDelegateMsg::ApplicationMessage(create_msg);
let outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![inbound])?;
let expected_payload =
bincode::serialize(&OutboundAppMessage::CreateInboxResponse(vec![1])).unwrap();
assert_eq!(outbound.len(), 1);
assert!(matches!(
outbound.first(),
Some(OutboundDelegateMsg::ApplicationMessage(msg)) if *msg.payload == expected_payload
));
let payload: Vec<u8> =
bincode::serialize(&InboundAppMessage::PleaseSignMessage(vec![1, 2, 3])).unwrap();
let sign_msg = ApplicationMessage::new(payload);
let inbound = InboundDelegateMsg::ApplicationMessage(sign_msg);
let outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![inbound])?;
let expected_payload =
bincode::serialize(&OutboundAppMessage::MessageSigned(vec![4, 5, 2])).unwrap();
assert_eq!(outbound.len(), 1);
assert!(matches!(
outbound.first(),
Some(OutboundDelegateMsg::ApplicationMessage(msg)) if *msg.payload == expected_payload
));
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_persistence_within_call() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let test_data = vec![1, 2, 3, 4, 5];
let write_payload =
bincode::serialize(&InboundAppMessage::WriteContext(test_data.clone()))?;
let read_payload = bincode::serialize(&InboundAppMessage::ReadContext)?;
let messages = vec![
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(write_payload)),
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(read_payload)),
];
let outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, messages)?;
assert_eq!(outbound.len(), 2);
let response1: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response1, OutboundAppMessage::ContextWritten));
let response2: OutboundAppMessage = match &outbound[1] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response2 {
OutboundAppMessage::ContextData(data) => {
assert_eq!(data, test_data);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected ContextData, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_reset_between_calls() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let payload = bincode::serialize(&InboundAppMessage::IncrementCounter)?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::CounterValue(1)));
let payload = bincode::serialize(&InboundAppMessage::IncrementCounter)?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::CounterValue(1)));
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_has_secret_host_function() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let secret_key = vec![10, 20, 30];
let secret_value = vec![100, 200];
let payload = bincode::serialize(&InboundAppMessage::HasSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretExists(false)));
let payload = bincode::serialize(&InboundAppMessage::StoreSecret {
key: secret_key.clone(),
value: secret_value.clone(),
})?;
let msg = ApplicationMessage::new(payload);
let _ = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let payload = bincode::serialize(&InboundAppMessage::HasSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretExists(true)));
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_nonexistent_secret() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let nonexistent_key = vec![99, 98, 97];
let payload =
bincode::serialize(&InboundAppMessage::GetNonExistentSecret(nonexistent_key))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretResult(None)));
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_store_and_retrieve_secret() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let secret_key = vec![42, 43, 44];
let secret_value = vec![1, 2, 3, 4, 5, 6, 7, 8];
let payload = bincode::serialize(&InboundAppMessage::StoreSecret {
key: secret_key.clone(),
value: secret_value.clone(),
})?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretStored));
let payload =
bincode::serialize(&InboundAppMessage::GetNonExistentSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::SecretResult(Some(value)) => {
assert_eq!(value, secret_value);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected SecretResult(Some(...)), got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_set_secret_failure_returns_secret_store_failed()
-> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let secrets_dir = temp_dir.path().join("secrets");
std::fs::set_permissions(&secrets_dir, std::fs::Permissions::from_mode(0o444))?;
let payload = bincode::serialize(&InboundAppMessage::StoreSecret {
key: vec![1, 2, 3],
value: vec![4, 5, 6],
})?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(
matches!(response, OutboundAppMessage::SecretStoreFailed),
"Expected SecretStoreFailed when secrets dir is read-only, got {:?}",
response
);
std::fs::set_permissions(&secrets_dir, std::fs::Permissions::from_mode(0o755))?;
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_empty_context() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let payload = bincode::serialize(&InboundAppMessage::ReadContext)?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::ContextData(data) => {
assert!(data.is_empty());
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected ContextData, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_clear() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let test_data = vec![1, 2, 3, 4, 5];
let payload = bincode::serialize(&InboundAppMessage::WriteContext(test_data.clone()))?;
let msg = ApplicationMessage::new(payload);
let _ = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let payload = bincode::serialize(&InboundAppMessage::ClearContext)?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::ContextCleared));
let payload = bincode::serialize(&InboundAppMessage::ReadContext)?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::ContextData(data) => {
assert!(data.is_empty());
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected ContextData, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_context_shared_across_batch() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let messages: Vec<InboundDelegateMsg> = (0..3)
.map(|_| {
let payload = bincode::serialize(&InboundAppMessage::IncrementCounter).unwrap();
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(payload))
})
.collect();
let outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, messages)?;
assert_eq!(outbound.len(), 3);
for (i, msg) in outbound.iter().enumerate() {
let response: OutboundAppMessage = match msg {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::CounterValue(value) => {
assert_eq!(value, (i + 1) as u32);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected CounterValue, got {:?}", other)
}
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_remove_secret_host_function() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let secret_key = vec![50, 51, 52];
let secret_value = vec![200, 201, 202];
let payload = bincode::serialize(&InboundAppMessage::StoreSecret {
key: secret_key.clone(),
value: secret_value.clone(),
})?;
let msg = ApplicationMessage::new(payload);
let _ = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let payload = bincode::serialize(&InboundAppMessage::HasSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretExists(true)));
let payload = bincode::serialize(&InboundAppMessage::RemoveSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretRemoved));
let payload = bincode::serialize(&InboundAppMessage::HasSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
assert!(matches!(response, OutboundAppMessage::SecretExists(false)));
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_large_context_data() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let large_size = 1024 * 1024;
let payload = bincode::serialize(&InboundAppMessage::WriteLargeContext(large_size))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::LargeContextWritten(size) => {
assert_eq!(size, large_size);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected LargeContextWritten, got {:?}", other)
}
}
let payload = bincode::serialize(&InboundAppMessage::ReadContext)?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::ContextData(data) => {
assert!(data.is_empty());
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected ContextData, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_large_context_within_batch() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let large_size = 256 * 1024;
let write_payload = bincode::serialize(&InboundAppMessage::WriteLargeContext(large_size))?;
let read_payload = bincode::serialize(&InboundAppMessage::ReadContext)?;
let messages = vec![
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(write_payload)),
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(read_payload)),
];
let outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, messages)?;
assert_eq!(outbound.len(), 2);
let response: OutboundAppMessage = match &outbound[1] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::ContextData(data) => {
assert_eq!(data.len(), large_size);
for (i, byte) in data.iter().enumerate() {
assert_eq!(*byte, (i % 256) as u8, "Data pattern mismatch at index {i}");
}
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected ContextData, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_large_secret_data() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_2).await?;
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let secret_key = vec![77, 88, 99];
let large_size = 1024 * 1024;
let payload = bincode::serialize(&InboundAppMessage::StoreLargeSecret {
key: secret_key.clone(),
size: large_size,
})?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::LargeSecretStored(size) => {
assert_eq!(size, large_size);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected LargeSecretStored, got {:?}", other)
}
}
let payload =
bincode::serialize(&InboundAppMessage::GetNonExistentSecret(secret_key.clone()))?;
let msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)?;
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => bincode::deserialize(&m.payload)?,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::SecretResult(Some(data)) => {
assert_eq!(data.len(), large_size);
for (i, byte) in data.iter().enumerate() {
assert_eq!(*byte, (i % 256) as u8, "Data pattern mismatch at index {i}");
}
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => {
panic!("Expected SecretResult(Some(...)), got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_concurrent_delegate_execution() -> Result<(), Box<dyn std::error::Error>> {
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
use std::sync::Arc;
use tokio::sync::Barrier;
let (delegate1, runtime1, temp_dir1) = setup_runtime(TEST_DELEGATE_2).await?;
let (delegate2, runtime2, temp_dir2) = setup_runtime(TEST_DELEGATE_2).await?;
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let runtime1 = Arc::new(std::sync::Mutex::new(runtime1));
let runtime2 = Arc::new(std::sync::Mutex::new(runtime2));
let delegate1 = Arc::new(delegate1);
let delegate2 = Arc::new(delegate2);
let barrier = Arc::new(Barrier::new(2));
let barrier1 = barrier.clone();
let runtime1_clone = runtime1.clone();
let delegate1_clone = delegate1.clone();
let handle1 = tokio::spawn(async move {
barrier1.wait().await;
let secret_key = b"thread1_key".to_vec();
let secret_value = b"thread1_value".to_vec();
let payload = bincode::serialize(&InboundAppMessage::StoreSecret {
key: secret_key.clone(),
value: secret_value.clone(),
})
.unwrap();
let msg = ApplicationMessage::new(payload);
{
let mut runtime = runtime1_clone.lock().unwrap();
let _ = runtime
.inbound_app_message(
delegate1_clone.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)
.unwrap();
}
let payload =
bincode::serialize(&InboundAppMessage::GetNonExistentSecret(secret_key.clone()))
.unwrap();
let msg = ApplicationMessage::new(payload);
let outbound = {
let mut runtime = runtime1_clone.lock().unwrap();
runtime
.inbound_app_message(
delegate1_clone.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)
.unwrap()
};
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => {
bincode::deserialize(&m.payload).unwrap()
}
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::SecretResult(Some(value)) => {
assert_eq!(value, secret_value);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => panic!(
"Thread 1: Expected SecretResult(Some(...)), got {:?}",
other
),
}
});
let barrier2 = barrier.clone();
let runtime2_clone = runtime2.clone();
let delegate2_clone = delegate2.clone();
let handle2 = tokio::spawn(async move {
barrier2.wait().await;
let secret_key = b"thread2_key".to_vec();
let secret_value = b"thread2_value".to_vec();
let payload = bincode::serialize(&InboundAppMessage::StoreSecret {
key: secret_key.clone(),
value: secret_value.clone(),
})
.unwrap();
let msg = ApplicationMessage::new(payload);
{
let mut runtime = runtime2_clone.lock().unwrap();
let _ = runtime
.inbound_app_message(
delegate2_clone.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)
.unwrap();
}
let payload =
bincode::serialize(&InboundAppMessage::GetNonExistentSecret(secret_key.clone()))
.unwrap();
let msg = ApplicationMessage::new(payload);
let outbound = {
let mut runtime = runtime2_clone.lock().unwrap();
runtime
.inbound_app_message(
delegate2_clone.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(msg)],
)
.unwrap()
};
let response: OutboundAppMessage = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => {
bincode::deserialize(&m.payload).unwrap()
}
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage")
}
};
match response {
OutboundAppMessage::SecretResult(Some(value)) => {
assert_eq!(value, secret_value);
}
other @ OutboundAppMessage::CreateInboxResponse(_)
| other @ OutboundAppMessage::MessageSigned(_)
| other @ OutboundAppMessage::ContextData(_)
| other @ OutboundAppMessage::CounterValue(_)
| other @ OutboundAppMessage::SecretExists(_)
| other @ OutboundAppMessage::SecretResult(_)
| other @ OutboundAppMessage::ContextWritten
| other @ OutboundAppMessage::ContextCleared
| other @ OutboundAppMessage::SecretStored
| other @ OutboundAppMessage::SecretRemoved
| other @ OutboundAppMessage::LargeContextWritten(_)
| other @ OutboundAppMessage::LargeSecretStored(_)
| other @ OutboundAppMessage::SecretStoreFailed => panic!(
"Thread 2: Expected SecretResult(Some(...)), got {:?}",
other
),
}
});
handle1.await?;
handle2.await?;
std::mem::drop(temp_dir1);
std::mem::drop(temp_dir2);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v1_delegate_detected_as_v1_with_state_store()
-> Result<(), Box<dyn std::error::Error>> {
use crate::contract::storages::Storage;
use delegate2_messages::{InboundAppMessage, OutboundAppMessage};
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db.clone())?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
runtime.set_state_store_db(db);
let delegate = {
let bytes = super::super::tests::get_test_module(TEST_DELEGATE_2)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
&vec![].into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
let (mut running, api_version) =
runtime.prepare_delegate_call(&vec![].into(), delegate.key(), 4096)?;
assert_eq!(
api_version,
DelegateApiVersion::V1,
"V1 delegate should be detected as V1 even with state_store_db configured"
);
runtime.drop_running_instance(&mut running);
let contract = WrappedContract::new(
Arc::new(ContractCode::from(vec![1])),
Parameters::from(vec![]),
);
let _app = ContractInstanceId::try_from(contract.key.to_string()).unwrap();
let payload: Vec<u8> = bincode::serialize(&InboundAppMessage::CreateInboxRequest).unwrap();
let create_msg = ApplicationMessage::new(payload);
let inbound = InboundDelegateMsg::ApplicationMessage(create_msg);
let outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![inbound])?;
let expected_payload =
bincode::serialize(&OutboundAppMessage::CreateInboxResponse(vec![1])).unwrap();
assert_eq!(outbound.len(), 1);
assert!(matches!(
outbound.first(),
Some(OutboundDelegateMsg::ApplicationMessage(msg)) if *msg.payload == expected_payload
));
std::mem::drop(temp_dir);
Ok(())
}
const TEST_DELEGATE_V2_CONTRACTS: &str = "test_delegate_v2_contracts";
mod v2_contracts_messages {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
pub enum InboundAppMessage {
GetContractState {
contract_id: [u8; 32],
},
PutContractState {
contract_id: [u8; 32],
state: Vec<u8>,
},
UpdateContractState {
contract_id: [u8; 32],
state: Vec<u8>,
},
SubscribeContract {
contract_id: [u8; 32],
},
}
#[derive(Debug, Serialize, Deserialize)]
pub enum OutboundAppMessage {
ContractState {
contract_id: [u8; 32],
state: Vec<u8>,
},
ContractNotFound {
contract_id: [u8; 32],
error_code: i64,
},
Success {
contract_id: [u8; 32],
},
Failed {
contract_id: [u8; 32],
error_code: i64,
},
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v2_delegate_reads_contract_state() -> Result<(), Box<dyn std::error::Error>> {
use crate::contract::storages::Storage;
use crate::wasm_runtime::StateStorage;
use v2_contracts_messages::*;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db.clone())?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
runtime.set_state_store_db(db.clone());
let contract_instance_id = ContractInstanceId::new([42u8; 32]);
let contract_code = ContractCode::from(vec![1, 2, 3]);
let contract_key =
ContractKey::from_id_and_code(contract_instance_id, *contract_code.hash());
let expected_state = vec![10, 20, 30, 40, 50, 60, 70, 80];
db.store(contract_key, WrappedState::new(expected_state.clone()))
.await?;
runtime.contract_store.ensure_key_indexed(&contract_key)?;
let delegate = {
let bytes = super::super::tests::get_test_module(TEST_DELEGATE_V2_CONTRACTS)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
&vec![].into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
let (mut running, api_version) =
runtime.prepare_delegate_call(&vec![].into(), delegate.key(), 4096)?;
assert_eq!(
api_version,
DelegateApiVersion::V2,
"V2 delegate should be detected as V2 (imports freenet_delegate_contracts)"
);
runtime.drop_running_instance(&mut running);
let _app_id = ContractInstanceId::new([1u8; 32]);
let command = InboundAppMessage::GetContractState {
contract_id: [42u8; 32],
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let response_msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(response_msg.processed);
let response: OutboundAppMessage = bincode::deserialize(&response_msg.payload)?;
match response {
OutboundAppMessage::ContractState { contract_id, state } => {
assert_eq!(contract_id, [42u8; 32]);
assert_eq!(
state, expected_state,
"V2 delegate should read contract state via host functions"
);
}
other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Success { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("V2 delegate returned {other:?} — expected ContractState");
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v2_delegate_contract_not_found() -> Result<(), Box<dyn std::error::Error>> {
use crate::contract::storages::Storage;
use v2_contracts_messages::*;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db.clone())?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
runtime.set_state_store_db(db);
let delegate = {
let bytes = super::super::tests::get_test_module(TEST_DELEGATE_V2_CONTRACTS)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
&vec![].into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
let _app_id = ContractInstanceId::new([1u8; 32]);
let command = InboundAppMessage::GetContractState {
contract_id: [99u8; 32],
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1);
let response_msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
let response: OutboundAppMessage = bincode::deserialize(&response_msg.payload)?;
match response {
OutboundAppMessage::ContractNotFound { error_code, .. } => {
assert!(
error_code < 0,
"Expected negative error code for not-found, got {error_code}"
);
}
other @ OutboundAppMessage::ContractState { .. }
| other @ OutboundAppMessage::Success { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("Expected ContractNotFound for non-existent contract, got {other:?}");
}
}
std::mem::drop(temp_dir);
Ok(())
}
async fn setup_v2_runtime_with_contract(
contract_id_byte: u8,
initial_state: Option<&[u8]>,
) -> Result<
(
DelegateContainer,
Runtime,
ContractInstanceId,
tempfile::TempDir,
),
Box<dyn std::error::Error>,
> {
use crate::contract::storages::Storage;
use crate::wasm_runtime::StateStorage;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db.clone())?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
runtime.set_state_store_db(db.clone());
let contract_instance_id = ContractInstanceId::new([contract_id_byte; 32]);
let contract_code = ContractCode::from(vec![contract_id_byte, 2, 3]);
let contract_key =
ContractKey::from_id_and_code(contract_instance_id, *contract_code.hash());
runtime.contract_store.ensure_key_indexed(&contract_key)?;
if let Some(state) = initial_state {
db.store(contract_key, WrappedState::new(state.to_vec()))
.await?;
}
let delegate = {
let bytes = super::super::tests::get_test_module(TEST_DELEGATE_V2_CONTRACTS)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
&vec![].into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
Ok((delegate, runtime, contract_instance_id, temp_dir))
}
fn send_v2_message(
runtime: &mut Runtime,
delegate: &DelegateContainer,
message: &v2_contracts_messages::InboundAppMessage,
) -> Result<v2_contracts_messages::OutboundAppMessage, Box<dyn std::error::Error>> {
let _app_id = ContractInstanceId::new([1u8; 32]);
let payload = bincode::serialize(message)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let response_msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(response_msg.processed);
Ok(bincode::deserialize(&response_msg.payload)?)
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v2_delegate_put_then_get() -> Result<(), Box<dyn std::error::Error>> {
use v2_contracts_messages::*;
let (delegate, mut runtime, contract_instance_id, _temp_dir) =
setup_v2_runtime_with_contract(50, None).await?;
let cid: [u8; 32] = contract_instance_id.as_bytes().try_into().unwrap();
let put_response = send_v2_message(
&mut runtime,
&delegate,
&InboundAppMessage::PutContractState {
contract_id: cid,
state: vec![100, 200, 150],
},
)?;
match put_response {
OutboundAppMessage::Success { contract_id } => {
assert_eq!(contract_id, cid);
}
other @ OutboundAppMessage::ContractState { .. }
| other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("Expected Success from PUT, got {:?}", other)
}
}
let get_response = send_v2_message(
&mut runtime,
&delegate,
&InboundAppMessage::GetContractState { contract_id: cid },
)?;
match get_response {
OutboundAppMessage::ContractState { contract_id, state } => {
assert_eq!(contract_id, cid);
assert_eq!(state, vec![100, 200, 150]);
}
other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Success { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("Expected ContractState from GET, got {:?}", other)
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v2_delegate_update_existing_state() -> Result<(), Box<dyn std::error::Error>> {
use v2_contracts_messages::*;
let (delegate, mut runtime, contract_instance_id, _temp_dir) =
setup_v2_runtime_with_contract(51, Some(&[1, 2, 3])).await?;
let cid: [u8; 32] = contract_instance_id.as_bytes().try_into().unwrap();
let update_response = send_v2_message(
&mut runtime,
&delegate,
&InboundAppMessage::UpdateContractState {
contract_id: cid,
state: vec![7, 8, 9],
},
)?;
match update_response {
OutboundAppMessage::Success { contract_id } => {
assert_eq!(contract_id, cid);
}
other @ OutboundAppMessage::ContractState { .. }
| other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("Expected Success from UPDATE, got {:?}", other)
}
}
let get_response = send_v2_message(
&mut runtime,
&delegate,
&InboundAppMessage::GetContractState { contract_id: cid },
)?;
match get_response {
OutboundAppMessage::ContractState { state, .. } => {
assert_eq!(state, vec![7, 8, 9]);
}
other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Success { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("Expected ContractState, got {:?}", other)
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v2_delegate_update_nonexistent_fails() -> Result<(), Box<dyn std::error::Error>> {
use v2_contracts_messages::*;
let (delegate, mut runtime, contract_instance_id, _temp_dir) =
setup_v2_runtime_with_contract(52, None).await?;
let cid: [u8; 32] = contract_instance_id.as_bytes().try_into().unwrap();
let response = send_v2_message(
&mut runtime,
&delegate,
&InboundAppMessage::UpdateContractState {
contract_id: cid,
state: vec![1, 2, 3],
},
)?;
match response {
OutboundAppMessage::Failed { error_code, .. } => {
assert!(
error_code < 0,
"Expected negative error code, got {error_code}"
);
}
other @ OutboundAppMessage::ContractState { .. }
| other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Success { .. } => panic!(
"Expected Failed from UPDATE on non-existent, got {:?}",
other
),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_v2_delegate_subscribe_known() -> Result<(), Box<dyn std::error::Error>> {
use v2_contracts_messages::*;
let (delegate, mut runtime, contract_instance_id, _temp_dir) =
setup_v2_runtime_with_contract(53, Some(&[1])).await?;
let cid: [u8; 32] = contract_instance_id.as_bytes().try_into().unwrap();
let response = send_v2_message(
&mut runtime,
&delegate,
&InboundAppMessage::SubscribeContract { contract_id: cid },
)?;
match response {
OutboundAppMessage::Success { contract_id } => {
assert_eq!(contract_id, cid);
}
other @ OutboundAppMessage::ContractState { .. }
| other @ OutboundAppMessage::ContractNotFound { .. }
| other @ OutboundAppMessage::Failed { .. } => {
panic!("Expected Success from SUBSCRIBE, got {:?}", other)
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_put_contract_request_response() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let _app_id = ContractInstanceId::new([1u8; 32]);
let code = ContractCode::from(vec![0u8; 10]);
let params = Parameters::from(vec![]);
let wrapped = WrappedContract::new(Arc::new(code), params);
let contract = ContractContainer::Wasm(ContractWasmAPIVersion::V1(wrapped));
let contract_key = contract.key();
let command = DelegateCommand::PutContractState {
contract,
state: vec![10, 20, 30],
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let put_request = match &outbound[0] {
OutboundDelegateMsg::PutContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected PutContractRequest, got {:?}", other)
}
};
assert_eq!(put_request.contract.key(), contract_key);
assert!(!put_request.processed);
let response = InboundDelegateMsg::PutContractResponse(PutContractResponse {
contract_id: *contract_key.id(),
result: Ok(()),
context: put_request.context.clone(),
});
let final_outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response])?;
assert_eq!(
final_outbound.len(),
1,
"Expected exactly one final message"
);
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(final_msg.processed);
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::ContractPutResult { success, error, .. } => {
assert!(success);
assert!(error.is_none());
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractPutResult, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_update_contract_request_response() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let contract_id = ContractInstanceId::new([42u8; 32]);
let _app_id = ContractInstanceId::new([1u8; 32]);
let command = DelegateCommand::UpdateContractState {
contract_id,
state: vec![10, 20, 30],
};
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let update_request = match &outbound[0] {
OutboundDelegateMsg::UpdateContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected UpdateContractRequest, got {:?}", other)
}
};
assert_eq!(update_request.contract_id, contract_id);
assert!(!update_request.processed);
let response = InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
contract_id,
result: Ok(()),
context: update_request.context.clone(),
});
let final_outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response])?;
assert_eq!(
final_outbound.len(),
1,
"Expected exactly one final message"
);
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(final_msg.processed);
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::ContractUpdateResult {
contract_id: id,
success,
error,
} => {
assert_eq!(id, contract_id);
assert!(success);
assert!(error.is_none());
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractUpdateResult, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_subscribe_contract_request_response() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let contract_id = ContractInstanceId::new([42u8; 32]);
let _app_id = ContractInstanceId::new([1u8; 32]);
let command = DelegateCommand::SubscribeContract { contract_id };
let payload = bincode::serialize(&command)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let subscribe_request = match &outbound[0] {
OutboundDelegateMsg::SubscribeContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected SubscribeContractRequest, got {:?}", other)
}
};
assert_eq!(subscribe_request.contract_id, contract_id);
assert!(!subscribe_request.processed);
let response = InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
contract_id,
result: Err("not yet implemented".to_string()),
context: subscribe_request.context.clone(),
});
let final_outbound =
runtime.inbound_app_message(delegate.key(), &vec![].into(), None, vec![response])?;
assert_eq!(
final_outbound.len(),
1,
"Expected exactly one final message"
);
let final_msg = match &final_outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(final_msg.processed);
let response: DelegateResponse = bincode::deserialize(&final_msg.payload)?;
match response {
DelegateResponse::ContractSubscribeResult {
contract_id: id,
success,
error,
} => {
assert_eq!(id, contract_id);
assert!(!success);
assert!(error.is_some());
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractSubscribeResult, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_contract_notification_delivered() -> Result<(), Box<dyn std::error::Error>> {
use capabilities_messages::*;
let (delegate, mut runtime, temp_dir) = setup_runtime(TEST_DELEGATE_CAPABILITIES).await?;
let contract_id = ContractInstanceId::new([42u8; 32]);
let new_state = vec![10, 20, 30, 40];
let notification = InboundDelegateMsg::ContractNotification(ContractNotification {
contract_id,
new_state: WrappedState::new(new_state.clone()),
context: DelegateContext::default(),
});
let outbound = runtime.inbound_app_message(
delegate.key(),
&vec![].into(),
None,
vec![notification],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
let msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(msg.processed);
let response: DelegateResponse = bincode::deserialize(&msg.payload)?;
match response {
DelegateResponse::ContractNotificationReceived {
contract_id: id,
new_state: state,
} => {
assert_eq!(id, contract_id);
assert_eq!(state, new_state);
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractNotificationReceived, got {:?}", other)
}
}
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_subscribe_then_notify_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
use crate::contract::storages::Storage;
use crate::wasm_runtime::StateStorage;
use capabilities_messages::*;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db.clone())?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
runtime.set_state_store_db(db.clone());
let contract_instance_id = ContractInstanceId::new([42u8; 32]);
let contract_code = ContractCode::from(vec![42, 2, 3]);
let contract_key =
ContractKey::from_id_and_code(contract_instance_id, *contract_code.hash());
runtime.contract_store.ensure_key_indexed(&contract_key)?;
db.store(contract_key, WrappedState::new(vec![1, 2, 3]))
.await?;
let delegate = {
let bytes = super::super::tests::get_test_module(TEST_DELEGATE_CAPABILITIES)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
&vec![].into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
let delegate_key = delegate.key().clone();
let _app_id = ContractInstanceId::new([1u8; 32]);
let subscribe_cmd = DelegateCommand::SubscribeContract {
contract_id: contract_instance_id,
};
let payload = bincode::serialize(&subscribe_cmd)?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
&delegate_key,
&vec![].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert_eq!(outbound.len(), 1);
let subscribe_req = match &outbound[0] {
OutboundDelegateMsg::SubscribeContractRequest(req) => req.clone(),
other @ OutboundDelegateMsg::ApplicationMessage(_)
| other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected SubscribeContractRequest, got {:?}", other)
}
};
assert_eq!(subscribe_req.contract_id, contract_instance_id);
let subscribe_result = if runtime
.contract_store
.code_hash_from_id(&subscribe_req.contract_id)
.is_some()
{
crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS
.entry(subscribe_req.contract_id)
.or_default()
.insert(delegate_key.clone());
Ok(())
} else {
Err("Contract not found".to_string())
};
assert!(
subscribe_result.is_ok(),
"Subscribe should succeed for known contract"
);
let subscribe_response =
InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
contract_id: subscribe_req.contract_id,
result: subscribe_result,
context: subscribe_req.context.clone(),
});
let outbound = runtime.inbound_app_message(
&delegate_key,
&vec![].into(),
None,
vec![subscribe_response],
)?;
assert_eq!(outbound.len(), 1);
match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => {
let resp: DelegateResponse = bincode::deserialize(&msg.payload)?;
match resp {
DelegateResponse::ContractSubscribeResult { success, .. } => {
assert!(success, "Subscribe response should indicate success");
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractNotificationReceived { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractSubscribeResult, got {:?}", other)
}
}
}
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
}
{
let entry = crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS.get(&contract_instance_id);
let subscribers = entry.as_ref().unwrap();
assert!(
subscribers.contains(&delegate_key),
"Delegate should be registered as subscriber"
);
}
let unknown_id = ContractInstanceId::new([99u8; 32]);
let has_code = runtime
.contract_store
.code_hash_from_id(&unknown_id)
.is_some();
assert!(!has_code, "Unknown contract should not be in store");
let updated_state = vec![10, 20, 30, 40, 50];
let notification = InboundDelegateMsg::ContractNotification(ContractNotification {
contract_id: contract_instance_id,
new_state: WrappedState::new(updated_state.clone()),
context: DelegateContext::default(),
});
let outbound =
runtime.inbound_app_message(&delegate_key, &vec![].into(), None, vec![notification])?;
assert_eq!(outbound.len(), 1, "Expected one outbound from notification");
let msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
other @ OutboundDelegateMsg::RequestUserInput(_)
| other @ OutboundDelegateMsg::ContextUpdated(_)
| other @ OutboundDelegateMsg::GetContractRequest(_)
| other @ OutboundDelegateMsg::PutContractRequest(_)
| other @ OutboundDelegateMsg::UpdateContractRequest(_)
| other @ OutboundDelegateMsg::SubscribeContractRequest(_)
| other @ OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", other)
}
};
assert!(msg.processed);
let response: DelegateResponse = bincode::deserialize(&msg.payload)?;
match response {
DelegateResponse::ContractNotificationReceived {
contract_id: id,
new_state: state,
} => {
assert_eq!(id, contract_instance_id);
assert_eq!(state, updated_state);
}
other @ DelegateResponse::ContractState { .. }
| other @ DelegateResponse::MultipleContractStates { .. }
| other @ DelegateResponse::Echo { .. }
| other @ DelegateResponse::ContractPutResult { .. }
| other @ DelegateResponse::ContractUpdateResult { .. }
| other @ DelegateResponse::ContractSubscribeResult { .. }
| other @ DelegateResponse::Error { .. } => {
panic!("Expected ContractNotificationReceived, got {:?}", other)
}
}
crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS.retain(|_, subscribers| {
subscribers.remove(&delegate_key);
!subscribers.is_empty()
});
let entry = crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS.get(&contract_instance_id);
assert!(
entry.is_none() || entry.as_ref().unwrap().is_empty(),
"Subscription should be cleaned up after delegate unregister"
);
std::mem::drop(temp_dir);
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_contract_removal_cleans_subscriptions() -> Result<(), Box<dyn std::error::Error>>
{
use crate::contract::storages::Storage;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db.clone())?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
runtime.set_state_store_db(db.clone());
let contract_instance_id = ContractInstanceId::new([99u8; 32]);
let contract_code = ContractCode::from(vec![99, 2, 3]);
let contract_key =
ContractKey::from_id_and_code(contract_instance_id, *contract_code.hash());
let wasm_path = runtime.contract_store.get_contract_path(&contract_key)?;
std::fs::create_dir_all(wasm_path.parent().unwrap())?;
std::fs::write(&wasm_path, [0u8; 10])?;
runtime.contract_store.ensure_key_indexed(&contract_key)?;
let delegate_key_a = DelegateKey::new([1u8; 32], CodeHash::new([10u8; 32]));
let delegate_key_b = DelegateKey::new([2u8; 32], CodeHash::new([20u8; 32]));
{
let mut entry = crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS
.entry(contract_instance_id)
.or_default();
entry.insert(delegate_key_a);
entry.insert(delegate_key_b);
}
assert!(
crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS
.get(&contract_instance_id)
.is_some()
);
runtime.contract_store.remove_contract(&contract_key)?;
assert!(
crate::wasm_runtime::DELEGATE_SUBSCRIPTIONS
.get(&contract_instance_id)
.is_none(),
"DELEGATE_SUBSCRIPTIONS should be cleaned up when contract is removed"
);
std::mem::drop(temp_dir);
Ok(())
}
const TEST_DELEGATE_MESSAGING: &str = "test_delegate_messaging";
mod messaging_messages {
use super::*;
#[derive(Debug, Serialize, Deserialize)]
pub enum InboundAppMessage {
SendToDelegate {
target_key_bytes: Vec<u8>,
target_code_hash: Vec<u8>,
payload: Vec<u8>,
},
Ping {
data: Vec<u8>,
},
}
#[derive(Debug, Serialize, Deserialize)]
pub enum OutboundAppMessage {
MessageSent,
DelegateMessageReceived {
sender_key_bytes: Vec<u8>,
payload: Vec<u8>,
origin_delegate_key_bytes: Option<Vec<u8>>,
},
PingResponse {
data: Vec<u8>,
},
}
}
async fn setup_runtime_with_params(
name: &str,
params: Vec<u8>,
) -> Result<(DelegateContainer, Runtime, tempfile::TempDir), Box<dyn std::error::Error>> {
use crate::contract::storages::Storage;
let temp_dir = get_temp_dir();
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 db = Storage::new(temp_dir.path()).await?;
let contract_store = ContractStore::new(contracts_dir, 10_000, db.clone())?;
let delegate_store = DelegateStore::new(delegates_dir, 10_000, db.clone())?;
let secret_store = SecretsStore::new(secrets_dir, Default::default(), db)?;
let mut runtime =
Runtime::build(contract_store, delegate_store, secret_store, false).unwrap();
let delegate = {
let bytes = super::super::tests::get_test_module(name)?;
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((
&bytes.into(),
¶ms.into(),
))))
};
let _stored = runtime.delegate_store.store_delegate(delegate.clone());
let key = XChaCha20Poly1305::generate_key(&mut OsRng);
let cipher = XChaCha20Poly1305::new(&key);
let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng);
let _registered =
runtime
.secret_store
.register_delegate(delegate.key().clone(), cipher, nonce);
Ok((delegate, runtime, temp_dir))
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delegate_emits_send_delegate_message() -> Result<(), Box<dyn std::error::Error>> {
use messaging_messages::*;
let (delegate_a, mut runtime, _temp_dir) =
setup_runtime_with_params(TEST_DELEGATE_MESSAGING, vec![1]).await?;
let key_a = delegate_a.key().clone();
let target_key_bytes = vec![42u8; 32];
let target_code_hash = vec![99u8; 32];
let _app_id = ContractInstanceId::new([1u8; 32]);
let payload = bincode::serialize(&InboundAppMessage::SendToDelegate {
target_key_bytes: target_key_bytes.clone(),
target_code_hash: target_code_hash.clone(),
payload: b"hello".to_vec(),
})?;
let app_msg = ApplicationMessage::new(payload);
let outbound = runtime.inbound_app_message(
&key_a,
&vec![1u8].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
assert!(
!outbound.is_empty(),
"Expected at least 1 outbound message, got {}",
outbound.len()
);
let send_msg = outbound
.iter()
.find_map(|m| match m {
OutboundDelegateMsg::SendDelegateMessage(msg) => Some(msg),
OutboundDelegateMsg::ApplicationMessage(_)
| OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_) => None,
})
.expect("Expected SendDelegateMessage in outbound");
let mut expected_key = [0u8; 32];
expected_key.copy_from_slice(&target_key_bytes);
let mut expected_hash = [0u8; 32];
expected_hash.copy_from_slice(&target_code_hash);
assert_eq!(*send_msg.target, expected_key);
assert_eq!(send_msg.target.code_hash(), &CodeHash::new(expected_hash));
assert_eq!(
send_msg.sender, key_a,
"Sender should be attested as delegate A"
);
assert_eq!(send_msg.payload, b"hello");
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delegate_receives_delegate_message() -> Result<(), Box<dyn std::error::Error>> {
use messaging_messages::*;
let (delegate_b, mut runtime, _temp_dir) =
setup_runtime_with_params(TEST_DELEGATE_MESSAGING, vec![2]).await?;
let key_b = delegate_b.key().clone();
let sender_key = DelegateKey::new([11u8; 32], CodeHash::new([22u8; 32]));
let delegate_msg =
DelegateMessage::new(key_b.clone(), sender_key.clone(), b"hello".to_vec());
let outbound = runtime.inbound_app_message(
&key_b,
&vec![2u8].into(),
None,
vec![InboundDelegateMsg::DelegateMessage(delegate_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected 1 outbound message");
let app_msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!("Expected ApplicationMessage, got {:?}", &outbound[0])
}
};
let response: OutboundAppMessage = bincode::deserialize(&app_msg.payload)?;
match response {
OutboundAppMessage::DelegateMessageReceived {
sender_key_bytes,
payload,
origin_delegate_key_bytes,
} => {
assert_eq!(sender_key_bytes, sender_key.bytes());
assert_eq!(payload, b"hello");
assert!(
origin_delegate_key_bytes.is_none(),
"origin was None, so receiver should see no Delegate origin"
);
}
OutboundAppMessage::MessageSent | OutboundAppMessage::PingResponse { .. } => {
panic!("Expected DelegateMessageReceived, got {:?}", response)
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_inbound_app_message_propagates_delegate_origin()
-> Result<(), Box<dyn std::error::Error>> {
use messaging_messages::*;
let (delegate_b, mut runtime, _temp_dir) =
setup_runtime_with_params(TEST_DELEGATE_MESSAGING, vec![2]).await?;
let key_b = delegate_b.key().clone();
let caller_a = DelegateKey::new([0xA1u8; 32], CodeHash::new([0xA2u8; 32]));
let inband_sender = DelegateKey::new([0xBBu8; 32], CodeHash::new([0xCCu8; 32]));
let delegate_msg =
DelegateMessage::new(key_b.clone(), inband_sender, b"attest-me".to_vec());
let origin = MessageOrigin::Delegate(caller_a.clone());
let outbound = runtime.inbound_app_message(
&key_b,
&vec![2u8].into(),
Some(&origin),
vec![InboundDelegateMsg::DelegateMessage(delegate_msg)],
)?;
assert_eq!(outbound.len(), 1, "Expected exactly one outbound message");
#[allow(clippy::wildcard_enum_match_arm)]
let app_msg = match &outbound[0] {
OutboundDelegateMsg::ApplicationMessage(m) => m,
other => panic!("Expected ApplicationMessage, got {other:?}"),
};
let response: OutboundAppMessage = bincode::deserialize(&app_msg.payload)?;
#[allow(clippy::wildcard_enum_match_arm)]
match response {
OutboundAppMessage::DelegateMessageReceived {
origin_delegate_key_bytes,
..
} => {
let observed = origin_delegate_key_bytes
.expect("Receiver should see Some(MessageOrigin::Delegate(..))");
assert_eq!(
observed,
caller_a.bytes(),
"Receiver must see the runtime-attested caller key, not msg.sender"
);
}
other => panic!("Expected DelegateMessageReceived, got {other:?}"),
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delegate_to_delegate_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
use messaging_messages::*;
let (delegate_a, mut runtime_a, _temp_dir_a) =
setup_runtime_with_params(TEST_DELEGATE_MESSAGING, vec![1]).await?;
let key_a = delegate_a.key().clone();
let (delegate_b, mut runtime_b, _temp_dir_b) =
setup_runtime_with_params(TEST_DELEGATE_MESSAGING, vec![2]).await?;
let key_b = delegate_b.key().clone();
let _app_id = ContractInstanceId::new([1u8; 32]);
let payload = bincode::serialize(&InboundAppMessage::SendToDelegate {
target_key_bytes: key_b.bytes().to_vec(),
target_code_hash: key_b.code_hash().as_ref().to_vec(),
payload: b"inter-delegate".to_vec(),
})?;
let app_msg = ApplicationMessage::new(payload);
let outbound_a = runtime_a.inbound_app_message(
&key_a,
&vec![1u8].into(),
None,
vec![InboundDelegateMsg::ApplicationMessage(app_msg)],
)?;
let send_msg = outbound_a
.iter()
.find_map(|m| match m {
OutboundDelegateMsg::SendDelegateMessage(msg) => Some(msg.clone()),
OutboundDelegateMsg::ApplicationMessage(_)
| OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_) => None,
})
.expect("Expected SendDelegateMessage from delegate A");
assert_eq!(send_msg.sender, key_a, "Sender should be attested as A");
assert_eq!(send_msg.payload, b"inter-delegate");
let outbound_b = runtime_b.inbound_app_message(
&key_b,
&vec![2u8].into(),
None,
vec![InboundDelegateMsg::DelegateMessage(send_msg)],
)?;
assert_eq!(outbound_b.len(), 1);
let app_msg_b = match &outbound_b[0] {
OutboundDelegateMsg::ApplicationMessage(msg) => msg,
OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_)
| OutboundDelegateMsg::SendDelegateMessage(_) => {
panic!(
"Expected ApplicationMessage from B, got {:?}",
&outbound_b[0]
)
}
};
let response: OutboundAppMessage = bincode::deserialize(&app_msg_b.payload)?;
match response {
OutboundAppMessage::DelegateMessageReceived {
sender_key_bytes,
payload,
origin_delegate_key_bytes,
} => {
assert_eq!(
sender_key_bytes,
key_a.bytes(),
"B should see A as the sender"
);
assert_eq!(payload, b"inter-delegate");
let _ = origin_delegate_key_bytes;
}
OutboundAppMessage::MessageSent | OutboundAppMessage::PingResponse { .. } => {
panic!("Expected DelegateMessageReceived, got {:?}", response)
}
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multiple_send_delegate_messages_all_attested()
-> Result<(), Box<dyn std::error::Error>> {
let (delegate_a, mut runtime, _temp_dir) =
setup_runtime_with_params(TEST_DELEGATE_MESSAGING, vec![1]).await?;
let key_a = delegate_a.key().clone();
let target_b = DelegateKey::new([42u8; 32], CodeHash::new([99u8; 32]));
let target_c = DelegateKey::new([43u8; 32], CodeHash::new([98u8; 32]));
let _app_id = ContractInstanceId::new([1u8; 32]);
let payload1 =
bincode::serialize(&messaging_messages::InboundAppMessage::SendToDelegate {
target_key_bytes: target_b.bytes().to_vec(),
target_code_hash: target_b.code_hash().as_ref().to_vec(),
payload: b"msg1".to_vec(),
})?;
let payload2 =
bincode::serialize(&messaging_messages::InboundAppMessage::SendToDelegate {
target_key_bytes: target_c.bytes().to_vec(),
target_code_hash: target_c.code_hash().as_ref().to_vec(),
payload: b"msg2".to_vec(),
})?;
let outbound = runtime.inbound_app_message(
&key_a,
&vec![1u8].into(),
None,
vec![
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(payload1)),
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(payload2)),
],
)?;
let send_msgs: Vec<&DelegateMessage> = outbound
.iter()
.filter_map(|m| match m {
OutboundDelegateMsg::SendDelegateMessage(msg) => Some(msg),
OutboundDelegateMsg::ApplicationMessage(_)
| OutboundDelegateMsg::RequestUserInput(_)
| OutboundDelegateMsg::ContextUpdated(_)
| OutboundDelegateMsg::GetContractRequest(_)
| OutboundDelegateMsg::PutContractRequest(_)
| OutboundDelegateMsg::UpdateContractRequest(_)
| OutboundDelegateMsg::SubscribeContractRequest(_) => None,
})
.collect();
assert!(
!send_msgs.is_empty(),
"Expected at least one SendDelegateMessage"
);
for (i, msg) in send_msgs.iter().enumerate() {
assert_eq!(
msg.sender, key_a,
"SendDelegateMessage[{i}] sender should be attested as delegate A, \
but got {:?}",
msg.sender
);
}
Ok(())
}
}