use bytes::Bytes;
use flatbuffers::WIPOffset;
use std::borrow::Cow;
use std::fmt::Display;
use std::net::SocketAddr;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::client_api::TryFromFbs;
use crate::generated::client_request::{
root_as_client_request, ClientRequestType, ContractRequest as FbsContractRequest,
ContractRequestType, DelegateRequest as FbsDelegateRequest, DelegateRequestType,
};
use crate::generated::common::{
ApplicationMessage as FbsApplicationMessage, ApplicationMessageArgs, ContractCode,
ContractCodeArgs, ContractContainer as FbsContractContainer, ContractContainerArgs,
ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
ContractKey as FbsContractKey, ContractKeyArgs, ContractType, DeltaUpdate, DeltaUpdateArgs,
RelatedDeltaUpdate, RelatedDeltaUpdateArgs, RelatedStateAndDeltaUpdate,
RelatedStateAndDeltaUpdateArgs, RelatedStateUpdate, RelatedStateUpdateArgs,
StateAndDeltaUpdate, StateAndDeltaUpdateArgs, StateUpdate, StateUpdateArgs,
UpdateData as FbsUpdateData, UpdateDataArgs, UpdateDataType, WasmContractV1,
WasmContractV1Args,
};
use crate::generated::host_response::{
finish_host_response_buffer, ClientResponse as FbsClientResponse, ClientResponseArgs,
ContextUpdated as FbsContextUpdated, ContextUpdatedArgs,
ContractResponse as FbsContractResponse, ContractResponseArgs, ContractResponseType,
DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateResponse as FbsDelegateResponse,
DelegateResponseArgs, GetResponse as FbsGetResponse, GetResponseArgs,
HostResponse as FbsHostResponse, HostResponseArgs, HostResponseType, NotFound as FbsNotFound,
NotFoundArgs, Ok as FbsOk, OkArgs, OutboundDelegateMsg as FbsOutboundDelegateMsg,
OutboundDelegateMsgArgs, OutboundDelegateMsgType, PutResponse as FbsPutResponse,
PutResponseArgs, RequestUserInput as FbsRequestUserInput, RequestUserInputArgs,
StreamChunk as FbsHostStreamChunk, StreamChunkArgs as FbsHostStreamChunkArgs,
UpdateNotification as FbsUpdateNotification, UpdateNotificationArgs,
UpdateResponse as FbsUpdateResponse, UpdateResponseArgs,
};
use crate::prelude::ContractContainer::Wasm;
use crate::prelude::ContractWasmAPIVersion::V1;
use crate::prelude::UpdateData::{
Delta, RelatedDelta, RelatedState, RelatedStateAndDelta, State, StateAndDelta,
};
use crate::{
delegate_interface::{DelegateKey, InboundDelegateMsg, OutboundDelegateMsg},
prelude::{
ContractInstanceId, ContractKey, DelegateContainer, Parameters, RelatedContracts,
SecretsId, StateSummary, UpdateData, WrappedState,
},
versioning::ContractContainer,
};
use super::WsApiError;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ClientError {
kind: Box<ErrorKind>,
}
impl ClientError {
pub fn into_fbs_bytes(self) -> Result<Vec<u8>, Box<ClientError>> {
use crate::generated::host_response::{Error, ErrorArgs};
let mut builder = flatbuffers::FlatBufferBuilder::new();
let msg_offset = builder.create_string(&self.to_string());
let err_offset = Error::create(
&mut builder,
&ErrorArgs {
msg: Some(msg_offset),
},
);
let host_response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::Ok,
response: Some(err_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, host_response_offset);
Ok(builder.finished_data().to_vec())
}
pub fn kind(&self) -> &ErrorKind {
&self.kind
}
}
impl From<ErrorKind> for ClientError {
fn from(kind: ErrorKind) -> Self {
ClientError {
kind: Box::new(kind),
}
}
}
impl<T: Into<Cow<'static, str>>> From<T> for ClientError {
fn from(cause: T) -> Self {
ClientError {
kind: Box::new(ErrorKind::Unhandled {
cause: cause.into(),
}),
}
}
}
#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone)]
#[non_exhaustive]
pub enum ErrorKind {
#[error("comm channel between client/host closed")]
ChannelClosed,
#[error("error while deserializing: {cause}")]
DeserializationError { cause: Cow<'static, str> },
#[error("client disconnected")]
Disconnect,
#[error("failed while trying to unpack state for {0}")]
IncorrectState(ContractKey),
#[error("node not available")]
NodeUnavailable,
#[error("lost the connection with the protocol handling connections")]
TransportProtocolDisconnect,
#[error("unhandled error: {cause}")]
Unhandled { cause: Cow<'static, str> },
#[error("unknown client id: {0}")]
UnknownClient(usize),
#[error(transparent)]
RequestError(#[from] RequestError),
#[error("error while executing operation in the network: {cause}")]
OperationError { cause: Cow<'static, str> },
#[error("operation timed out")]
FailedOperation,
#[error("peer should shutdown")]
Shutdown,
#[error("no ring connections found")]
EmptyRing,
#[error("peer has not joined the network yet")]
PeerNotJoined,
}
impl Display for ClientError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "client error: {}", self.kind)
}
}
impl std::error::Error for ClientError {}
#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
#[non_exhaustive]
pub enum RequestError {
#[error(transparent)]
ContractError(#[from] ContractError),
#[error(transparent)]
DelegateError(#[from] DelegateError),
#[error("client disconnect")]
Disconnect,
#[error("operation timed out")]
Timeout,
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
#[non_exhaustive]
pub enum DelegateError {
#[error("error while registering delegate {0}")]
RegisterError(DelegateKey),
#[error("execution error, cause {0}")]
ExecutionError(Cow<'static, str>),
#[error("missing delegate {0}")]
Missing(DelegateKey),
#[error("missing secret `{secret}` for delegate {key}")]
MissingSecret { key: DelegateKey, secret: SecretsId },
#[error("forbidden access to secret: {0}")]
ForbiddenSecretAccess(SecretsId),
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
#[non_exhaustive]
pub enum ContractError {
#[error("failed to get contract {key}, reason: {cause}")]
Get {
key: ContractKey,
cause: Cow<'static, str>,
},
#[error("put error for contract {key}, reason: {cause}")]
Put {
key: ContractKey,
cause: Cow<'static, str>,
},
#[error("update error for contract {key}, reason: {cause}")]
Update {
key: ContractKey,
cause: Cow<'static, str>,
},
#[error("failed to subscribe for contract {key}, reason: {cause}")]
Subscribe {
key: ContractKey,
cause: Cow<'static, str>,
},
#[error("dependency contract stack overflow : {key}")]
ContractStackOverflow {
key: crate::contract_interface::ContractInstanceId,
},
#[error("missing related contract: {key}")]
MissingRelated {
key: crate::contract_interface::ContractInstanceId,
},
#[error("missing contract: {key}")]
MissingContract {
key: crate::contract_interface::ContractInstanceId,
},
}
impl ContractError {
const EXECUTION_ERROR: &'static str = "execution error";
const INVALID_PUT: &'static str = "invalid put";
pub fn update_exec_error(key: ContractKey, additional_info: impl std::fmt::Display) -> Self {
Self::Update {
key,
cause: format!(
"{exec_err}: {additional_info}",
exec_err = Self::EXECUTION_ERROR
)
.into(),
}
}
pub fn invalid_put(key: ContractKey) -> Self {
Self::Put {
key,
cause: Self::INVALID_PUT.into(),
}
}
pub fn invalid_update(key: ContractKey) -> Self {
Self::Update {
key,
cause: Self::INVALID_PUT.into(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub enum ClientRequest<'a> {
DelegateOp(#[serde(borrow)] DelegateRequest<'a>),
ContractOp(#[serde(borrow)] ContractRequest<'a>),
Disconnect {
cause: Option<Cow<'static, str>>,
},
Authenticate {
token: String,
},
NodeQueries(NodeQuery),
Close,
StreamChunk {
stream_id: u32,
index: u32,
total: u32,
data: Bytes,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ConnectedPeers {}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NodeDiagnostics {
pub contract_key: Option<ContractKey>,
}
impl ClientRequest<'_> {
pub fn into_owned(self) -> ClientRequest<'static> {
match self {
ClientRequest::ContractOp(op) => {
let owned = match op {
ContractRequest::Put {
contract,
state,
related_contracts,
subscribe,
blocking_subscribe,
} => {
let related_contracts = related_contracts.into_owned();
ContractRequest::Put {
contract,
state,
related_contracts,
subscribe,
blocking_subscribe,
}
}
ContractRequest::Update { key, data } => {
let data = data.into_owned();
ContractRequest::Update { key, data }
}
ContractRequest::Get {
key,
return_contract_code,
subscribe,
blocking_subscribe,
} => ContractRequest::Get {
key,
return_contract_code,
subscribe,
blocking_subscribe,
},
ContractRequest::Subscribe { key, summary } => ContractRequest::Subscribe {
key,
summary: summary.map(StateSummary::into_owned),
},
};
owned.into()
}
ClientRequest::DelegateOp(op) => {
let op = op.into_owned();
ClientRequest::DelegateOp(op)
}
ClientRequest::Disconnect { cause } => ClientRequest::Disconnect { cause },
ClientRequest::Authenticate { token } => ClientRequest::Authenticate { token },
ClientRequest::NodeQueries(query) => ClientRequest::NodeQueries(query),
ClientRequest::Close => ClientRequest::Close,
ClientRequest::StreamChunk {
stream_id,
index,
total,
data,
} => ClientRequest::StreamChunk {
stream_id,
index,
total,
data,
},
}
}
pub fn is_disconnect(&self) -> bool {
matches!(self, Self::Disconnect { .. })
}
pub fn try_decode_fbs(msg: &[u8]) -> Result<ClientRequest<'_>, WsApiError> {
let req = {
match root_as_client_request(msg) {
Ok(client_request) => match client_request.client_request_type() {
ClientRequestType::ContractRequest => {
let contract_request =
client_request.client_request_as_contract_request().unwrap();
ContractRequest::try_decode_fbs(&contract_request)?.into()
}
ClientRequestType::DelegateRequest => {
let delegate_request =
client_request.client_request_as_delegate_request().unwrap();
DelegateRequest::try_decode_fbs(&delegate_request)?.into()
}
ClientRequestType::Disconnect => {
let delegate_request =
client_request.client_request_as_disconnect().unwrap();
let cause = delegate_request
.cause()
.map(|cause_msg| cause_msg.to_string().into());
ClientRequest::Disconnect { cause }
}
ClientRequestType::Authenticate => {
let auth_req = client_request.client_request_as_authenticate().unwrap();
let token = auth_req.token();
ClientRequest::Authenticate {
token: token.to_owned(),
}
}
ClientRequestType::StreamChunk => {
let chunk = client_request.client_request_as_stream_chunk().unwrap();
ClientRequest::StreamChunk {
stream_id: chunk.stream_id(),
index: chunk.index(),
total: chunk.total(),
data: Bytes::from(chunk.data().bytes().to_vec()),
}
}
other => {
return Err(crate::client_api::unknown_union_discriminant(
"ClientRequestType",
other.0,
))
}
},
Err(e) => {
let cause = format!("{e}");
return Err(WsApiError::deserialization(cause));
}
}
};
Ok(req)
}
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContractRequest<'a> {
Put {
contract: ContractContainer,
state: WrappedState,
#[serde(borrow)]
related_contracts: RelatedContracts<'a>,
subscribe: bool,
#[serde(default)]
blocking_subscribe: bool,
},
Update {
key: ContractKey,
#[serde(borrow)]
data: UpdateData<'a>,
},
Get {
key: ContractInstanceId,
return_contract_code: bool,
subscribe: bool,
#[serde(default)]
blocking_subscribe: bool,
},
Subscribe {
key: ContractInstanceId,
summary: Option<StateSummary<'a>>,
},
}
impl ContractRequest<'_> {
pub fn into_owned(self) -> ContractRequest<'static> {
match self {
Self::Put {
contract,
state,
related_contracts,
subscribe,
blocking_subscribe,
} => ContractRequest::Put {
contract,
state,
related_contracts: related_contracts.into_owned(),
subscribe,
blocking_subscribe,
},
Self::Update { key, data } => ContractRequest::Update {
key,
data: data.into_owned(),
},
Self::Get {
key,
return_contract_code: fetch_contract,
subscribe,
blocking_subscribe,
} => ContractRequest::Get {
key,
return_contract_code: fetch_contract,
subscribe,
blocking_subscribe,
},
Self::Subscribe { key, summary } => ContractRequest::Subscribe {
key,
summary: summary.map(StateSummary::into_owned),
},
}
}
}
impl<'a> From<ContractRequest<'a>> for ClientRequest<'a> {
fn from(op: ContractRequest<'a>) -> Self {
ClientRequest::ContractOp(op)
}
}
impl<'a> TryFromFbs<&FbsContractRequest<'a>> for ContractRequest<'a> {
fn try_decode_fbs(request: &FbsContractRequest<'a>) -> Result<Self, WsApiError> {
let req = {
match request.contract_request_type() {
ContractRequestType::Get => {
let get = request.contract_request_as_get().unwrap();
let fbs_key = get.key();
let key = crate::contract_interface::key::instance_id_from_fbs(
"ContractKey.instance.data",
fbs_key.instance().data().bytes(),
)?;
let fetch_contract = get.fetch_contract();
let subscribe = get.subscribe();
let blocking_subscribe = get.blocking_subscribe();
ContractRequest::Get {
key,
return_contract_code: fetch_contract,
subscribe,
blocking_subscribe,
}
}
ContractRequestType::Put => {
let put = request.contract_request_as_put().unwrap();
let contract = ContractContainer::try_decode_fbs(&put.container())?;
let state = WrappedState::new(put.wrapped_state().bytes().to_vec());
let related_contracts =
RelatedContracts::try_decode_fbs(&put.related_contracts())?.into_owned();
let subscribe = put.subscribe();
let blocking_subscribe = put.blocking_subscribe();
ContractRequest::Put {
contract,
state,
related_contracts,
subscribe,
blocking_subscribe,
}
}
ContractRequestType::Update => {
let update = request.contract_request_as_update().unwrap();
let key = ContractKey::try_decode_fbs(&update.key())?;
let data = UpdateData::try_decode_fbs(&update.data())?.into_owned();
ContractRequest::Update { key, data }
}
ContractRequestType::Subscribe => {
let subscribe = request.contract_request_as_subscribe().unwrap();
let fbs_key = subscribe.key();
let key = crate::contract_interface::key::instance_id_from_fbs(
"ContractKey.instance.data",
fbs_key.instance().data().bytes(),
)?;
let summary = subscribe
.summary()
.map(|summary_data| StateSummary::from(summary_data.bytes()));
ContractRequest::Subscribe { key, summary }
}
other => {
return Err(crate::client_api::unknown_union_discriminant(
"ContractRequestType",
other.0,
));
}
}
};
Ok(req)
}
}
impl<'a> From<DelegateRequest<'a>> for ClientRequest<'a> {
fn from(op: DelegateRequest<'a>) -> Self {
ClientRequest::DelegateOp(op)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub enum DelegateRequest<'a> {
ApplicationMessages {
key: DelegateKey,
#[serde(deserialize_with = "Parameters::deser_params")]
params: Parameters<'a>,
#[serde(borrow)]
inbound: Vec<InboundDelegateMsg<'a>>,
},
RegisterDelegate {
delegate: DelegateContainer,
cipher: [u8; 32],
nonce: [u8; 24],
},
UnregisterDelegate(DelegateKey),
RegisterDelegateWithPredecessors {
delegate: DelegateContainer,
cipher: [u8; 32],
nonce: [u8; 24],
predecessors: Vec<DelegateKey>,
},
}
impl DelegateRequest<'_> {
pub fn into_owned(self) -> DelegateRequest<'static> {
match self {
DelegateRequest::ApplicationMessages {
key,
inbound,
params,
} => DelegateRequest::ApplicationMessages {
key,
params: params.into_owned(),
inbound: inbound.into_iter().map(|e| e.into_owned()).collect(),
},
DelegateRequest::RegisterDelegate {
delegate,
cipher,
nonce,
} => DelegateRequest::RegisterDelegate {
delegate,
cipher,
nonce,
},
DelegateRequest::UnregisterDelegate(key) => DelegateRequest::UnregisterDelegate(key),
DelegateRequest::RegisterDelegateWithPredecessors {
delegate,
cipher,
nonce,
predecessors,
} => DelegateRequest::RegisterDelegateWithPredecessors {
delegate,
cipher,
nonce,
predecessors,
},
}
}
pub fn key(&self) -> &DelegateKey {
match self {
DelegateRequest::ApplicationMessages { key, .. } => key,
DelegateRequest::RegisterDelegate { delegate, .. } => delegate.key(),
DelegateRequest::UnregisterDelegate(key) => key,
DelegateRequest::RegisterDelegateWithPredecessors { delegate, .. } => delegate.key(),
}
}
}
impl Display for ClientRequest<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ClientRequest::ContractOp(op) => match op {
ContractRequest::Put {
contract, state, ..
} => {
write!(
f,
"ContractRequest::Put for contract `{contract}` with state {state}"
)
}
ContractRequest::Update { key, .. } => write!(f, "update request for {key}"),
ContractRequest::Get {
key,
return_contract_code: contract,
..
} => {
write!(
f,
"ContractRequest::Get for key `{key}` (fetch full contract: {contract})"
)
}
ContractRequest::Subscribe { key, .. } => {
write!(f, "ContractRequest::Subscribe for `{key}`")
}
},
ClientRequest::DelegateOp(op) => match op {
DelegateRequest::ApplicationMessages { key, inbound, .. } => {
write!(
f,
"DelegateRequest::ApplicationMessages for `{key}` with {} messages",
inbound.len()
)
}
DelegateRequest::RegisterDelegate { delegate, .. } => {
write!(
f,
"DelegateRequest::RegisterDelegate for delegate.key()=`{}`",
delegate.key()
)
}
DelegateRequest::UnregisterDelegate(key) => {
write!(f, "DelegateRequest::UnregisterDelegate for key `{key}`")
}
DelegateRequest::RegisterDelegateWithPredecessors {
delegate,
predecessors,
..
} => {
write!(
f,
"DelegateRequest::RegisterDelegateWithPredecessors for delegate.key()=`{}` with {} predecessor(s)",
delegate.key(),
predecessors.len()
)
}
},
ClientRequest::Disconnect { .. } => write!(f, "client disconnected"),
ClientRequest::Authenticate { .. } => write!(f, "authenticate"),
ClientRequest::NodeQueries(query) => write!(f, "node queries: {:?}", query),
ClientRequest::Close => write!(f, "close"),
ClientRequest::StreamChunk {
stream_id,
index,
total,
..
} => write!(f, "stream chunk {index}/{total} (stream {stream_id})"),
}
}
}
impl<'a> TryFromFbs<&FbsDelegateRequest<'a>> for DelegateRequest<'a> {
fn try_decode_fbs(request: &FbsDelegateRequest<'a>) -> Result<Self, WsApiError> {
let req = {
match request.delegate_request_type() {
DelegateRequestType::ApplicationMessages => {
let app_msg = request.delegate_request_as_application_messages().unwrap();
let key = DelegateKey::try_decode_fbs(&app_msg.key())?;
let params = Parameters::from(app_msg.params().bytes());
let inbound = app_msg
.inbound()
.iter()
.map(|msg| InboundDelegateMsg::try_decode_fbs(&msg))
.collect::<Result<Vec<_>, _>>()?;
DelegateRequest::ApplicationMessages {
key,
params,
inbound,
}
}
DelegateRequestType::RegisterDelegate => {
let register = request.delegate_request_as_register_delegate().unwrap();
let delegate = DelegateContainer::try_decode_fbs(®ister.delegate())?;
let cipher = crate::client_api::fixed_size_field::<32>(
"RegisterDelegate.cipher",
register.cipher().bytes(),
)?;
let nonce = crate::client_api::fixed_size_field::<24>(
"RegisterDelegate.nonce",
register.nonce().bytes(),
)?;
DelegateRequest::RegisterDelegate {
delegate,
cipher,
nonce,
}
}
DelegateRequestType::UnregisterDelegate => {
let unregister = request.delegate_request_as_unregister_delegate().unwrap();
let key = DelegateKey::try_decode_fbs(&unregister.key())?;
DelegateRequest::UnregisterDelegate(key)
}
other => {
return Err(crate::client_api::unknown_union_discriminant(
"DelegateRequestType",
other.0,
));
}
}
};
Ok(req)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub enum HostResponse<T = WrappedState> {
ContractResponse(#[serde(bound(deserialize = "T: DeserializeOwned"))] ContractResponse<T>),
DelegateResponse {
key: DelegateKey,
values: Vec<OutboundDelegateMsg>,
},
QueryResponse(QueryResponse),
Ok,
StreamChunk {
stream_id: u32,
index: u32,
total: u32,
data: Bytes,
},
StreamHeader {
stream_id: u32,
total_bytes: u64,
content: StreamContent,
},
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum StreamContent {
GetResponse {
key: ContractKey,
includes_contract: bool,
},
Raw,
}
type Peer = String;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum QueryResponse {
ConnectedPeers { peers: Vec<(Peer, SocketAddr)> },
NetworkDebug(NetworkDebugInfo),
NodeDiagnostics(NodeDiagnosticsResponse),
NeighborHosting(NeighborHostingInfo),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NetworkDebugInfo {
pub subscriptions: Vec<SubscriptionInfo>,
pub connected_peers: Vec<(String, SocketAddr)>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NodeDiagnosticsResponse {
pub node_info: Option<NodeInfo>,
pub network_info: Option<NetworkInfo>,
pub subscriptions: Vec<SubscriptionInfo>,
pub contract_states: std::collections::HashMap<String, ContractState>,
pub system_metrics: Option<SystemMetrics>,
pub connected_peers_detailed: Vec<ConnectedPeerInfo>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NodeInfo {
pub peer_id: String,
pub is_gateway: bool,
pub location: Option<String>,
pub listening_address: Option<String>,
pub uptime_seconds: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NetworkInfo {
pub connected_peers: Vec<(String, String)>, pub active_connections: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ContractState {
pub subscribers: u32,
pub subscriber_peer_ids: Vec<String>,
#[serde(default)]
pub size_bytes: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SystemMetrics {
pub active_connections: u32,
pub hosting_contracts: u32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubscriptionInfo {
pub contract_key: ContractInstanceId,
pub client_id: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ConnectedPeerInfo {
pub peer_id: String,
pub address: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum NodeQuery {
ConnectedPeers,
SubscriptionInfo,
NodeDiagnostics {
config: NodeDiagnosticsConfig,
},
NeighborHostingInfo,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NeighborHostingInfo {
pub my_hosted: Vec<ContractHostingEntry>,
pub neighbor_hosting: Vec<NeighborHostingDetail>,
pub stats: HostingStats,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ContractHostingEntry {
pub contract_key: String,
pub hosting_hash: u32,
pub hosted_since: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NeighborHostingDetail {
pub peer_id: String,
pub known_contracts: Vec<u32>,
pub last_update: u64,
pub update_count: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct HostingStats {
pub hosting_announces_sent: u64,
pub hosting_announces_received: u64,
pub updates_via_proximity: u64,
pub updates_via_subscription: u64,
pub false_positive_forwards: u64,
pub avg_neighbor_hosting_size: f32,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NodeDiagnosticsConfig {
pub include_node_info: bool,
pub include_network_info: bool,
pub include_subscriptions: bool,
pub contract_keys: Vec<ContractKey>,
pub include_system_metrics: bool,
pub include_detailed_peer_info: bool,
pub include_subscriber_peer_ids: bool,
}
impl NodeDiagnosticsConfig {
pub fn for_update_propagation_debugging(contract_key: ContractKey) -> Self {
Self {
include_node_info: true,
include_network_info: true,
include_subscriptions: true,
contract_keys: vec![contract_key],
include_system_metrics: true,
include_detailed_peer_info: true,
include_subscriber_peer_ids: true,
}
}
pub fn basic_status() -> Self {
Self {
include_node_info: true,
include_network_info: true,
include_subscriptions: false,
contract_keys: vec![],
include_system_metrics: false,
include_detailed_peer_info: false,
include_subscriber_peer_ids: false,
}
}
pub fn full() -> Self {
Self {
include_node_info: true,
include_network_info: true,
include_subscriptions: true,
contract_keys: vec![], include_system_metrics: true,
include_detailed_peer_info: true,
include_subscriber_peer_ids: true,
}
}
}
impl HostResponse {
pub fn unwrap_put(self) -> ContractKey {
if let Self::ContractResponse(ContractResponse::PutResponse { key }) = self {
key
} else {
panic!("called `HostResponse::unwrap_put()` on other than `PutResponse` value")
}
}
pub fn unwrap_get(self) -> (WrappedState, Option<ContractContainer>) {
if let Self::ContractResponse(ContractResponse::GetResponse {
contract, state, ..
}) = self
{
(state, contract)
} else {
panic!("called `HostResponse::unwrap_put()` on other than `PutResponse` value")
}
}
pub fn into_fbs_bytes(self) -> Result<Vec<u8>, Box<ClientError>> {
let mut builder = flatbuffers::FlatBufferBuilder::new();
match self {
HostResponse::ContractResponse(res) => match res {
ContractResponse::PutResponse { key } => {
let instance_data = builder.create_vector(key.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(builder.create_vector(&key.code_hash().0));
let key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let put_offset = FbsPutResponse::create(
&mut builder,
&PutResponseArgs {
key: Some(key_offset),
},
);
let contract_response_offset = FbsContractResponse::create(
&mut builder,
&ContractResponseArgs {
contract_response: Some(put_offset.as_union_value()),
contract_response_type: ContractResponseType::PutResponse,
},
);
let response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response: Some(contract_response_offset.as_union_value()),
response_type: HostResponseType::ContractResponse,
},
);
finish_host_response_buffer(&mut builder, response_offset);
Ok(builder.finished_data().to_vec())
}
ContractResponse::UpdateResponse { key, summary } => {
let instance_data = builder.create_vector(key.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(builder.create_vector(&key.code_hash().0));
let key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let summary_data = builder.create_vector(&summary.into_bytes());
let update_response_offset = FbsUpdateResponse::create(
&mut builder,
&UpdateResponseArgs {
key: Some(key_offset),
summary: Some(summary_data),
},
);
let contract_response_offset = FbsContractResponse::create(
&mut builder,
&ContractResponseArgs {
contract_response: Some(update_response_offset.as_union_value()),
contract_response_type: ContractResponseType::UpdateResponse,
},
);
let response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response: Some(contract_response_offset.as_union_value()),
response_type: HostResponseType::ContractResponse,
},
);
finish_host_response_buffer(&mut builder, response_offset);
Ok(builder.finished_data().to_vec())
}
ContractResponse::GetResponse {
key,
contract: contract_container,
state,
} => {
let instance_data = builder.create_vector(key.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(builder.create_vector(&key.code_hash().0));
let key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let container_offset = if let Some(contract) = contract_container {
let data = builder.create_vector(contract.key().as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs { data: Some(data) },
);
let code = Some(builder.create_vector(&contract.key().code_hash().0));
let contract_key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let contract_data =
builder.create_vector(contract.clone().unwrap_v1().data.data());
let contract_code_hash =
builder.create_vector(&contract.clone().unwrap_v1().data.hash().0);
let contract_code_offset = ContractCode::create(
&mut builder,
&ContractCodeArgs {
data: Some(contract_data),
code_hash: Some(contract_code_hash),
},
);
let contract_params =
builder.create_vector(&contract.clone().params().into_bytes());
let contract_offset = match contract {
Wasm(V1(..)) => WasmContractV1::create(
&mut builder,
&WasmContractV1Args {
key: Some(contract_key_offset),
data: Some(contract_code_offset),
parameters: Some(contract_params),
},
),
};
Some(FbsContractContainer::create(
&mut builder,
&ContractContainerArgs {
contract_type: ContractType::WasmContractV1,
contract: Some(contract_offset.as_union_value()),
},
))
} else {
None
};
let state_data = builder.create_vector(&state);
let get_offset = FbsGetResponse::create(
&mut builder,
&GetResponseArgs {
key: Some(key_offset),
contract: container_offset,
state: Some(state_data),
},
);
let contract_response_offset = FbsContractResponse::create(
&mut builder,
&ContractResponseArgs {
contract_response_type: ContractResponseType::GetResponse,
contract_response: Some(get_offset.as_union_value()),
},
);
let response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response: Some(contract_response_offset.as_union_value()),
response_type: HostResponseType::ContractResponse,
},
);
finish_host_response_buffer(&mut builder, response_offset);
Ok(builder.finished_data().to_vec())
}
ContractResponse::UpdateNotification { key, update } => {
let instance_data = builder.create_vector(key.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(builder.create_vector(&key.code_hash().0));
let key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let update_data = match update {
State(state) => {
let state_data = builder.create_vector(&state.into_bytes());
let state_update_offset = StateUpdate::create(
&mut builder,
&StateUpdateArgs {
state: Some(state_data),
},
);
FbsUpdateData::create(
&mut builder,
&UpdateDataArgs {
update_data_type: UpdateDataType::StateUpdate,
update_data: Some(state_update_offset.as_union_value()),
},
)
}
Delta(delta) => {
let delta_data = builder.create_vector(&delta.into_bytes());
let update_offset = DeltaUpdate::create(
&mut builder,
&DeltaUpdateArgs {
delta: Some(delta_data),
},
);
FbsUpdateData::create(
&mut builder,
&UpdateDataArgs {
update_data_type: UpdateDataType::DeltaUpdate,
update_data: Some(update_offset.as_union_value()),
},
)
}
StateAndDelta { state, delta } => {
let state_data = builder.create_vector(&state.into_bytes());
let delta_data = builder.create_vector(&delta.into_bytes());
let update_offset = StateAndDeltaUpdate::create(
&mut builder,
&StateAndDeltaUpdateArgs {
state: Some(state_data),
delta: Some(delta_data),
},
);
FbsUpdateData::create(
&mut builder,
&UpdateDataArgs {
update_data_type: UpdateDataType::StateAndDeltaUpdate,
update_data: Some(update_offset.as_union_value()),
},
)
}
RelatedState { related_to, state } => {
let state_data = builder.create_vector(&state.into_bytes());
let instance_data = builder.create_vector(related_to.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let update_offset = RelatedStateUpdate::create(
&mut builder,
&RelatedStateUpdateArgs {
related_to: Some(instance_offset),
state: Some(state_data),
},
);
FbsUpdateData::create(
&mut builder,
&UpdateDataArgs {
update_data_type: UpdateDataType::RelatedStateUpdate,
update_data: Some(update_offset.as_union_value()),
},
)
}
RelatedDelta { related_to, delta } => {
let instance_data = builder.create_vector(related_to.as_bytes());
let delta_data = builder.create_vector(&delta.into_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let update_offset = RelatedDeltaUpdate::create(
&mut builder,
&RelatedDeltaUpdateArgs {
related_to: Some(instance_offset),
delta: Some(delta_data),
},
);
FbsUpdateData::create(
&mut builder,
&UpdateDataArgs {
update_data_type: UpdateDataType::RelatedDeltaUpdate,
update_data: Some(update_offset.as_union_value()),
},
)
}
RelatedStateAndDelta {
related_to,
state,
delta,
} => {
let instance_data = builder.create_vector(related_to.as_bytes());
let state_data = builder.create_vector(&state.into_bytes());
let delta_data = builder.create_vector(&delta.into_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let update_offset = RelatedStateAndDeltaUpdate::create(
&mut builder,
&RelatedStateAndDeltaUpdateArgs {
related_to: Some(instance_offset),
state: Some(state_data),
delta: Some(delta_data),
},
);
FbsUpdateData::create(
&mut builder,
&UpdateDataArgs {
update_data_type: UpdateDataType::RelatedStateAndDeltaUpdate,
update_data: Some(update_offset.as_union_value()),
},
)
}
};
let update_notification_offset = FbsUpdateNotification::create(
&mut builder,
&UpdateNotificationArgs {
key: Some(key_offset),
update: Some(update_data),
},
);
let put_response_offset = FbsContractResponse::create(
&mut builder,
&ContractResponseArgs {
contract_response_type: ContractResponseType::UpdateNotification,
contract_response: Some(update_notification_offset.as_union_value()),
},
);
let host_response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::ContractResponse,
response: Some(put_response_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, host_response_offset);
Ok(builder.finished_data().to_vec())
}
ContractResponse::SubscribeResponse { key, .. } => {
let instance_data = builder.create_vector(key.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(builder.create_vector(&key.code_hash().0));
let key_offset = FbsContractKey::create(
&mut builder,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let put_offset = FbsPutResponse::create(
&mut builder,
&PutResponseArgs {
key: Some(key_offset),
},
);
let contract_response_offset = FbsContractResponse::create(
&mut builder,
&ContractResponseArgs {
contract_response_type: ContractResponseType::PutResponse,
contract_response: Some(put_offset.as_union_value()),
},
);
let host_response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::ContractResponse,
response: Some(contract_response_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, host_response_offset);
Ok(builder.finished_data().to_vec())
}
ContractResponse::NotFound { instance_id } => {
let instance_data = builder.create_vector(instance_id.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut builder,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let not_found_offset = FbsNotFound::create(
&mut builder,
&NotFoundArgs {
instance_id: Some(instance_offset),
},
);
let contract_response_offset = FbsContractResponse::create(
&mut builder,
&ContractResponseArgs {
contract_response_type: ContractResponseType::NotFound,
contract_response: Some(not_found_offset.as_union_value()),
},
);
let response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response: Some(contract_response_offset.as_union_value()),
response_type: HostResponseType::ContractResponse,
},
);
finish_host_response_buffer(&mut builder, response_offset);
Ok(builder.finished_data().to_vec())
}
},
HostResponse::DelegateResponse { key, values } => {
let key_data = builder.create_vector(key.bytes());
let code_hash_data = builder.create_vector(&key.code_hash().0);
let key_offset = FbsDelegateKey::create(
&mut builder,
&DelegateKeyArgs {
key: Some(key_data),
code_hash: Some(code_hash_data),
},
);
let mut messages: Vec<WIPOffset<FbsOutboundDelegateMsg>> = Vec::new();
values.iter().for_each(|msg| match msg {
OutboundDelegateMsg::ApplicationMessage(app) => {
let payload_data = builder.create_vector(&app.payload);
let delegate_context_data = builder.create_vector(app.context.as_ref());
let app_offset = FbsApplicationMessage::create(
&mut builder,
&ApplicationMessageArgs {
payload: Some(payload_data),
context: Some(delegate_context_data),
processed: app.processed,
},
);
let msg = FbsOutboundDelegateMsg::create(
&mut builder,
&OutboundDelegateMsgArgs {
inbound_type: OutboundDelegateMsgType::common_ApplicationMessage,
inbound: Some(app_offset.as_union_value()),
},
);
messages.push(msg);
}
OutboundDelegateMsg::RequestUserInput(input) => {
let message_data = builder.create_vector(input.message.bytes());
let mut responses: Vec<WIPOffset<FbsClientResponse>> = Vec::new();
input.responses.iter().for_each(|resp| {
let response_data = builder.create_vector(resp.bytes());
let response = FbsClientResponse::create(
&mut builder,
&ClientResponseArgs {
data: Some(response_data),
},
);
responses.push(response)
});
let responses_offset = builder.create_vector(&responses);
let input_offset = FbsRequestUserInput::create(
&mut builder,
&RequestUserInputArgs {
request_id: input.request_id,
message: Some(message_data),
responses: Some(responses_offset),
},
);
let msg = FbsOutboundDelegateMsg::create(
&mut builder,
&OutboundDelegateMsgArgs {
inbound_type: OutboundDelegateMsgType::RequestUserInput,
inbound: Some(input_offset.as_union_value()),
},
);
messages.push(msg);
}
OutboundDelegateMsg::ContextUpdated(context) => {
let context_data = builder.create_vector(context.as_ref());
let context_offset = FbsContextUpdated::create(
&mut builder,
&ContextUpdatedArgs {
context: Some(context_data),
},
);
let msg = FbsOutboundDelegateMsg::create(
&mut builder,
&OutboundDelegateMsgArgs {
inbound_type: OutboundDelegateMsgType::ContextUpdated,
inbound: Some(context_offset.as_union_value()),
},
);
messages.push(msg);
}
OutboundDelegateMsg::GetContractRequest(_) => {
tracing::error!(
"GetContractRequest reached client serialization - this is a bug"
);
}
OutboundDelegateMsg::PutContractRequest(_) => {
tracing::error!(
"PutContractRequest reached client serialization - this is a bug"
);
}
OutboundDelegateMsg::UpdateContractRequest(_) => {
tracing::error!(
"UpdateContractRequest reached client serialization - this is a bug"
);
}
OutboundDelegateMsg::SubscribeContractRequest(_) => {
tracing::error!(
"SubscribeContractRequest reached client serialization - this is a bug"
);
}
OutboundDelegateMsg::SendDelegateMessage(_) => {
tracing::error!(
"SendDelegateMessage reached client serialization - this is a bug"
);
}
});
let messages_offset = builder.create_vector(&messages);
let delegate_response_offset = FbsDelegateResponse::create(
&mut builder,
&DelegateResponseArgs {
key: Some(key_offset),
values: Some(messages_offset),
},
);
let host_response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::DelegateResponse,
response: Some(delegate_response_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, host_response_offset);
Ok(builder.finished_data().to_vec())
}
HostResponse::Ok => {
let ok_offset = FbsOk::create(&mut builder, &OkArgs { msg: None });
let host_response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::Ok,
response: Some(ok_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, host_response_offset);
Ok(builder.finished_data().to_vec())
}
HostResponse::QueryResponse(_) => unimplemented!(),
HostResponse::StreamChunk {
stream_id,
index,
total,
data,
} => {
let data_offset = builder.create_vector(&data);
let chunk_offset = FbsHostStreamChunk::create(
&mut builder,
&FbsHostStreamChunkArgs {
stream_id,
index,
total,
data: Some(data_offset),
},
);
let host_response_offset = FbsHostResponse::create(
&mut builder,
&HostResponseArgs {
response_type: HostResponseType::StreamChunk,
response: Some(chunk_offset.as_union_value()),
},
);
finish_host_response_buffer(&mut builder, host_response_offset);
Ok(builder.finished_data().to_vec())
}
HostResponse::StreamHeader { .. } => {
Err(Box::new(ClientError::from(ErrorKind::Unhandled {
cause: "StreamHeader is not supported over flatbuffers encoding".into(),
})))
}
}
}
}
impl Display for HostResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HostResponse::ContractResponse(res) => match res {
ContractResponse::PutResponse { key } => {
f.write_fmt(format_args!("put response for `{key}`"))
}
ContractResponse::UpdateResponse { key, .. } => {
f.write_fmt(format_args!("update response for `{key}`"))
}
ContractResponse::GetResponse { key, .. } => {
f.write_fmt(format_args!("get response for `{key}`"))
}
ContractResponse::UpdateNotification { key, .. } => {
f.write_fmt(format_args!("update notification for `{key}`"))
}
ContractResponse::SubscribeResponse { key, .. } => {
f.write_fmt(format_args!("subscribe response for `{key}`"))
}
ContractResponse::NotFound { instance_id } => {
f.write_fmt(format_args!("not found for `{instance_id}`"))
}
},
HostResponse::DelegateResponse { .. } => write!(f, "delegate responses"),
HostResponse::Ok => write!(f, "ok response"),
HostResponse::QueryResponse(_) => write!(f, "query response"),
HostResponse::StreamChunk {
stream_id,
index,
total,
..
} => write!(f, "stream chunk {index}/{total} (stream {stream_id})"),
HostResponse::StreamHeader {
stream_id,
total_bytes,
..
} => write!(f, "stream header (stream {stream_id}, {total_bytes} bytes)"),
}
}
}
#[derive(Clone, Serialize, Deserialize, Debug)]
#[non_exhaustive]
pub enum ContractResponse<T = WrappedState> {
GetResponse {
key: ContractKey,
contract: Option<ContractContainer>,
#[serde(bound(deserialize = "T: DeserializeOwned"))]
state: T,
},
PutResponse {
key: ContractKey,
},
UpdateNotification {
key: ContractKey,
#[serde(deserialize_with = "UpdateData::deser_update_data")]
update: UpdateData<'static>,
},
UpdateResponse {
key: ContractKey,
#[serde(deserialize_with = "StateSummary::deser_state_summary")]
summary: StateSummary<'static>,
},
SubscribeResponse {
key: ContractKey,
subscribed: bool,
},
NotFound {
instance_id: ContractInstanceId,
},
}
impl<T> From<ContractResponse<T>> for HostResponse<T> {
fn from(value: ContractResponse<T>) -> HostResponse<T> {
HostResponse::ContractResponse(value)
}
}
#[cfg(test)]
mod node_diagnostics_response_tests {
use super::{
ConnectedPeerInfo, ContractState, NetworkInfo, NodeDiagnosticsResponse, NodeInfo,
SubscriptionInfo, SystemMetrics,
};
use crate::contract_interface::ContractInstanceId;
use std::collections::HashMap;
#[test]
fn node_diagnostics_response_json_round_trips() {
let mut contract_states = HashMap::new();
contract_states.insert(
"6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9".to_string(),
ContractState {
subscribers: 3,
subscriber_peer_ids: vec!["peer-a".to_string(), "peer-b".to_string()],
size_bytes: 1024,
},
);
let response = NodeDiagnosticsResponse {
node_info: Some(NodeInfo {
peer_id: "peer-self".to_string(),
is_gateway: true,
location: Some("0.5".to_string()),
listening_address: Some("0.0.0.0:31337".to_string()),
uptime_seconds: 3600,
}),
network_info: Some(NetworkInfo {
connected_peers: vec![("peer-x".to_string(), "10.0.0.1:31337".to_string())],
active_connections: 1,
}),
subscriptions: vec![SubscriptionInfo {
contract_key: ContractInstanceId::new([7u8; 32]),
client_id: 42,
}],
contract_states,
system_metrics: Some(SystemMetrics {
active_connections: 1,
hosting_contracts: 1,
}),
connected_peers_detailed: vec![ConnectedPeerInfo {
peer_id: "peer-x".to_string(),
address: "10.0.0.1:31337".to_string(),
}],
};
let json = serde_json::to_string(&response).expect("must serialize to JSON");
let parsed: serde_json::Value = serde_json::from_str(&json).expect("output is valid JSON");
let obj = parsed.as_object().expect("top-level must be object");
assert_eq!(obj.len(), 6, "expected six top-level fields, got {obj:?}");
assert_eq!(parsed["node_info"]["peer_id"], "peer-self");
assert_eq!(parsed["network_info"]["active_connections"], 1);
assert_eq!(parsed["subscriptions"][0]["client_id"], 42);
assert_eq!(parsed["system_metrics"]["hosting_contracts"], 1);
assert_eq!(parsed["connected_peers_detailed"][0]["peer_id"], "peer-x");
let states = parsed["contract_states"]
.as_object()
.expect("contract_states must be a JSON object");
assert_eq!(states.len(), 1);
assert_eq!(
states["6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9"]["subscribers"],
3
);
let bytes = bincode::serialize(&response).expect("bincode must serialize");
let decoded: NodeDiagnosticsResponse =
bincode::deserialize(&bytes).expect("bincode must round-trip");
assert_eq!(
decoded.contract_states.len(),
1,
"bincode round-trip preserves contract_states entries"
);
}
}
#[cfg(test)]
mod client_request_test {
use crate::client_api::{ContractRequest, TryFromFbs, WsApiError};
use crate::contract_interface::UpdateData;
use crate::generated::client_request::root_as_client_request;
const EXPECTED_ENCODED_CONTRACT_ID: &str = "6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9";
#[test]
fn test_build_contract_put_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
let put_req_op = vec![
4, 0, 0, 0, 244, 255, 255, 255, 16, 0, 0, 0, 0, 0, 0, 1, 8, 0, 12, 0, 11, 0, 4, 0, 8,
0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 198, 255, 255, 255, 12, 0, 0, 0, 20, 0, 0, 0, 36, 0,
0, 0, 170, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8,
8, 0, 10, 0, 9, 0, 4, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 1, 10, 0, 16, 0, 12, 0, 8, 0, 4,
0, 10, 0, 0, 0, 12, 0, 0, 0, 76, 0, 0, 0, 92, 0, 0, 0, 176, 255, 255, 255, 8, 0, 0, 0,
16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0,
85, 111, 11, 171, 40, 85, 240, 177, 207, 81, 106, 157, 173, 90, 234, 2, 250, 253, 75,
210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17, 108, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6,
7, 8, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 8, 0, 0, 0, 1, 2,
3, 4, 5, 6, 7, 8, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8,
];
let request = if let Ok(client_request) = root_as_client_request(&put_req_op) {
let contract_request = client_request.client_request_as_contract_request().unwrap();
ContractRequest::try_decode_fbs(&contract_request)?
} else {
panic!("failed to decode client request")
};
match request {
ContractRequest::Put {
contract,
state,
related_contracts: _,
subscribe,
blocking_subscribe,
} => {
assert_eq!(
contract.to_string(),
"WasmContainer([api=0.0.1](D8fdVLbRyMLw5mZtPRpWMFcrXGN2z8Nq8UGcLGPFBg2W))"
);
assert_eq!(contract.unwrap_v1().data.data(), &[1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(state.to_vec(), &[1, 2, 3, 4, 5, 6, 7, 8]);
assert!(!subscribe);
assert!(!blocking_subscribe);
}
_ => panic!("wrong contract request type"),
}
Ok(())
}
#[test]
fn test_build_contract_get_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
let get_req_op = vec![
4, 0, 0, 0, 244, 255, 255, 255, 16, 0, 0, 0, 0, 0, 0, 1, 8, 0, 12, 0, 11, 0, 4, 0, 8,
0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 3, 222, 255, 255, 255, 12, 0, 0, 0, 8, 0, 12, 0, 8, 0, 4,
0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0,
4, 0, 0, 0, 32, 0, 0, 0, 85, 111, 11, 171, 40, 85, 240, 177, 207, 81, 106, 157, 173,
90, 234, 2, 250, 253, 75, 210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17, 108,
];
let request = if let Ok(client_request) = root_as_client_request(&get_req_op) {
let contract_request = client_request.client_request_as_contract_request().unwrap();
ContractRequest::try_decode_fbs(&contract_request)?
} else {
panic!("failed to decode client request")
};
match request {
ContractRequest::Get {
key,
return_contract_code: fetch_contract,
subscribe,
blocking_subscribe,
} => {
assert_eq!(key.encode(), EXPECTED_ENCODED_CONTRACT_ID);
assert!(!fetch_contract);
assert!(!subscribe);
assert!(!blocking_subscribe);
}
_ => panic!("wrong contract request type"),
}
Ok(())
}
#[test]
fn test_build_contract_update_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
use crate::generated::client_request::{
finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
ContractRequestType, Update as FbsUpdate, UpdateArgs,
};
use crate::generated::common::{
ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
ContractKey as FbsContractKey, ContractKeyArgs, DeltaUpdate, DeltaUpdateArgs,
UpdateData as FbsUpdateData, UpdateDataArgs, UpdateDataType,
};
use crate::prelude::ContractInstanceId;
let instance_id = ContractInstanceId::try_from(EXPECTED_ENCODED_CONTRACT_ID.to_string())?;
let code_hash = [42u8; 32];
let delta_bytes = [1u8, 2, 3, 4, 5, 6, 7, 8];
let mut b = flatbuffers::FlatBufferBuilder::new();
let instance_data = b.create_vector(instance_id.as_bytes());
let instance_offset = FbsContractInstanceId::create(
&mut b,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(b.create_vector(&code_hash));
let key_offset = FbsContractKey::create(
&mut b,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let delta = b.create_vector(&delta_bytes);
let delta_offset = DeltaUpdate::create(&mut b, &DeltaUpdateArgs { delta: Some(delta) });
let update_data_offset = FbsUpdateData::create(
&mut b,
&UpdateDataArgs {
update_data_type: UpdateDataType::DeltaUpdate,
update_data: Some(delta_offset.as_union_value()),
},
);
let update_offset = FbsUpdate::create(
&mut b,
&UpdateArgs {
key: Some(key_offset),
data: Some(update_data_offset),
},
);
let contract_offset = FbsContractRequest::create(
&mut b,
&ContractRequestArgs {
contract_request_type: ContractRequestType::Update,
contract_request: Some(update_offset.as_union_value()),
},
);
let client_offset = FbsClientRequest::create(
&mut b,
&ClientRequestArgs {
client_request_type: ClientRequestType::ContractRequest,
client_request: Some(contract_offset.as_union_value()),
},
);
finish_client_request_buffer(&mut b, client_offset);
let update_op = b.finished_data().to_vec();
let request = if let Ok(client_request) = root_as_client_request(&update_op) {
let contract_request = client_request.client_request_as_contract_request().unwrap();
ContractRequest::try_decode_fbs(&contract_request)?
} else {
panic!("failed to decode client request")
};
match request {
ContractRequest::Update { key, data } => {
assert_eq!(key.encoded_contract_id(), EXPECTED_ENCODED_CONTRACT_ID);
assert_eq!(
key.code_hash().as_ref(),
&code_hash,
"the code hash must survive decode unchanged, not be re-hashed"
);
match data {
UpdateData::Delta(delta) => {
assert_eq!(delta.to_vec(), &delta_bytes)
}
_ => panic!("wrong update data type"),
}
}
_ => panic!("wrong contract request type"),
}
Ok(())
}
const TS_SDK_EXPECTED_UPDATE_REQ: &[u8] = &[
4, 0, 0, 0, 220, 255, 255, 255, 8, 0, 0, 0, 0, 0, 0, 1, 232, 255, 255, 255, 8, 0, 0, 0, 0,
0, 0, 2, 204, 255, 255, 255, 16, 0, 0, 0, 52, 0, 0, 0, 8, 0, 12, 0, 11, 0, 4, 0, 8, 0, 0,
0, 8, 0, 0, 0, 0, 0, 0, 2, 210, 255, 255, 255, 4, 0, 0, 0, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7,
8, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 8,
0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0, 85, 111, 11, 171, 40, 85, 240, 177, 207, 81,
106, 157, 173, 90, 234, 2, 250, 253, 75, 210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17,
108,
];
#[test]
fn typescript_sdk_instance_only_update_is_rejected_with_guidance() {
let client_request = root_as_client_request(TS_SDK_EXPECTED_UPDATE_REQ)
.expect("the TS SDK blob must still be a well-formed ClientRequest");
let contract_request = client_request
.client_request_as_contract_request()
.expect("the TS SDK blob must still be a ContractRequest");
let err = ContractRequest::try_decode_fbs(&contract_request)
.expect_err("an instance-id-only UPDATE must be rejected");
let msg = err.to_string();
assert!(
msg.contains("ContractKey.code") && msg.contains("got 0 bytes"),
"the TS SDK's zero-length code must be named explicitly, got: {msg}"
);
assert!(
msg.contains("new ContractKey(instance, code)"),
"the error must tell a TypeScript developer how to build the key, got: {msg}"
);
assert!(
msg.contains("4978"),
"the error must point at the tracking issue for the real fix, got: {msg}"
);
}
fn client_request_with_instance_len(instance_len: usize, subscribe: bool) -> Vec<u8> {
use crate::generated::client_request::{
finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
ContractRequestType, Get as FbsGet, GetArgs, Subscribe as FbsSubscribe, SubscribeArgs,
};
use crate::generated::common::{
ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
ContractKey as FbsContractKey, ContractKeyArgs,
};
let mut b = flatbuffers::FlatBufferBuilder::new();
let instance_data = b.create_vector(&vec![1u8; instance_len]);
let instance_offset = FbsContractInstanceId::create(
&mut b,
&ContractInstanceIdArgs {
data: Some(instance_data),
},
);
let code = Some(b.create_vector(&[42u8; 32]));
let key_offset = FbsContractKey::create(
&mut b,
&ContractKeyArgs {
instance: Some(instance_offset),
code,
},
);
let (request_type, request_offset) = if subscribe {
let sub = FbsSubscribe::create(
&mut b,
&SubscribeArgs {
key: Some(key_offset),
summary: None,
},
);
(ContractRequestType::Subscribe, sub.as_union_value())
} else {
let get = FbsGet::create(
&mut b,
&GetArgs {
key: Some(key_offset),
fetch_contract: false,
subscribe: false,
blocking_subscribe: false,
},
);
(ContractRequestType::Get, get.as_union_value())
};
let contract_offset = FbsContractRequest::create(
&mut b,
&ContractRequestArgs {
contract_request_type: request_type,
contract_request: Some(request_offset),
},
);
let client_offset = FbsClientRequest::create(
&mut b,
&ClientRequestArgs {
client_request_type: ClientRequestType::ContractRequest,
client_request: Some(contract_offset.as_union_value()),
},
);
finish_client_request_buffer(&mut b, client_offset);
b.finished_data().to_vec()
}
fn decode_client_request(bytes: &[u8]) -> Result<ContractRequest<'_>, WsApiError> {
let client_request =
root_as_client_request(bytes).expect("must be a well-formed ClientRequest");
let contract_request = client_request
.client_request_as_contract_request()
.expect("must be a ContractRequest");
ContractRequest::try_decode_fbs(&contract_request)
}
#[test]
fn get_with_wrong_length_instance_is_rejected_not_panicking() {
let bytes = client_request_with_instance_len(8, false);
let short = decode_client_request(&bytes)
.expect_err("a GET with an 8-byte instance must be rejected");
assert!(
short.to_string().contains("ContractKey.instance")
&& short.to_string().contains("got 8 bytes"),
"got: {short}"
);
let bytes = client_request_with_instance_len(64, false);
let long = decode_client_request(&bytes)
.expect_err("a GET with a 64-byte instance must be rejected");
assert!(long.to_string().contains("got 64 bytes"), "got: {long}");
}
#[test]
fn subscribe_with_wrong_length_instance_is_rejected_not_panicking() {
let bytes = client_request_with_instance_len(8, true);
let short = decode_client_request(&bytes)
.expect_err("a SUBSCRIBE with an 8-byte instance must be rejected");
assert!(
short.to_string().contains("ContractKey.instance")
&& short.to_string().contains("got 8 bytes"),
"got: {short}"
);
let bytes = client_request_with_instance_len(64, true);
let long = decode_client_request(&bytes)
.expect_err("a SUBSCRIBE with a 64-byte instance must be rejected");
assert!(long.to_string().contains("got 64 bytes"), "got: {long}");
}
#[test]
fn get_with_valid_instance_still_decodes() {
let bytes = client_request_with_instance_len(32, false);
let req = decode_client_request(&bytes).expect("a 32-byte instance must still decode");
assert!(
matches!(req, ContractRequest::Get { .. }),
"expected a Get, got {req:?}"
);
}
#[test]
fn fbs_decode_rejects_unknown_contract_discriminant() {
use crate::generated::client_request::{
finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
ContractRequestType, DelegateKey as FbsDelegateKey, DelegateKeyArgs,
UnregisterDelegate, UnregisterDelegateArgs,
};
let mut b = flatbuffers::FlatBufferBuilder::new();
let key = b.create_vector(&[0u8; 32]);
let code_hash = b.create_vector(&[0u8; 32]);
let dk = FbsDelegateKey::create(
&mut b,
&DelegateKeyArgs {
key: Some(key),
code_hash: Some(code_hash),
},
);
let dummy = UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
let contract = FbsContractRequest::create(
&mut b,
&ContractRequestArgs {
contract_request_type: ContractRequestType(99),
contract_request: Some(dummy.as_union_value()),
},
);
let client = FbsClientRequest::create(
&mut b,
&ClientRequestArgs {
client_request_type: ClientRequestType::ContractRequest,
client_request: Some(contract.as_union_value()),
},
);
finish_client_request_buffer(&mut b, client);
let bytes = b.finished_data().to_vec();
let client =
root_as_client_request(&bytes).expect("verifier accepts an unknown union discriminant");
let fbs_contract = client
.client_request_as_contract_request()
.expect("client_request is a ContractRequest");
assert!(
ContractRequest::try_decode_fbs(&fbs_contract).is_err(),
"an unknown ContractRequestType discriminant must be a clean \
per-request error, never a panic that downs the connection handler"
);
}
}
#[cfg(test)]
mod delegate_request_wire_format {
use super::DelegateRequest;
use crate::code_hash::CodeHash;
use crate::prelude::{
ApplicationMessage, Delegate, DelegateCode, DelegateContainer, DelegateKey,
DelegateWasmAPIVersion, InboundDelegateMsg, Parameters,
};
fn sample_container() -> DelegateContainer {
let code = DelegateCode::from(vec![1u8, 2, 3, 4]);
let params = Parameters::from(vec![9u8, 8, 7]);
DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((&code, ¶ms))))
}
fn sample_key(fill: u8) -> DelegateKey {
DelegateKey::new([fill; 32], CodeHash::new([fill.wrapping_add(1); 32]))
}
fn sample_app_messages() -> DelegateRequest<'static> {
DelegateRequest::ApplicationMessages {
key: DelegateKey::new([0x11; 32], CodeHash::new([0x22; 32])),
params: Parameters::from(vec![0xDE, 0xAD, 0xBE, 0xEF]),
inbound: vec![InboundDelegateMsg::ApplicationMessage(
ApplicationMessage::new(vec![0x01, 0x02, 0x03]),
)],
}
}
fn sample_register() -> DelegateRequest<'static> {
DelegateRequest::RegisterDelegate {
delegate: sample_container(),
cipher: [0x55; 32],
nonce: [0x66; 24],
}
}
fn sample_unregister() -> DelegateRequest<'static> {
DelegateRequest::UnregisterDelegate(DelegateKey::new([0x11; 32], CodeHash::new([0x22; 32])))
}
fn sample_register_with_predecessors() -> DelegateRequest<'static> {
DelegateRequest::RegisterDelegateWithPredecessors {
delegate: sample_container(),
cipher: [0x33; 32],
nonce: [0x44; 24],
predecessors: vec![sample_key(0xA0), sample_key(0xB0), sample_key(0xC0)],
}
}
#[test]
fn wire_format_is_frozen() {
const APP_MESSAGES: &[u8] = &[
0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34, 4, 0, 0, 0, 0, 0, 0, 0, 222, 173, 190, 239, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
const REGISTER: &[u8] = &[
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 4, 0, 0, 0, 0, 0,
0, 0, 1, 2, 3, 4, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5, 141, 135, 18, 213, 208,
81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160, 84, 50, 88, 111, 44, 39,
24, 219, 97, 92, 222, 20, 205, 248, 149, 154, 214, 38, 193, 144, 31, 141, 32, 222, 49,
197, 66, 237, 16, 98, 165, 72, 6, 11, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5,
141, 135, 18, 213, 208, 81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160,
84, 50, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 102, 102, 102, 102, 102, 102, 102, 102,
102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
];
const UNREGISTER: &[u8] = &[
2, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
34,
];
const REGISTER_WITH_PREDECESSORS: &[u8] = &[
3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 4, 0, 0, 0, 0, 0,
0, 0, 1, 2, 3, 4, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5, 141, 135, 18, 213, 208,
81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160, 84, 50, 88, 111, 44, 39,
24, 219, 97, 92, 222, 20, 205, 248, 149, 154, 214, 38, 193, 144, 31, 141, 32, 222, 49,
197, 66, 237, 16, 98, 165, 72, 6, 11, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5,
141, 135, 18, 213, 208, 81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160,
84, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,
51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 3, 0, 0, 0, 0, 0, 0, 0, 160,
160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160,
160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 161, 161, 161,
161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161,
161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 176, 176, 176, 176, 176,
176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176,
176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 177, 177, 177, 177, 177, 177, 177,
177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177,
177, 177, 177, 177, 177, 177, 177, 177, 192, 192, 192, 192, 192, 192, 192, 192, 192,
192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
192, 192, 192, 192, 192, 192, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193,
193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193,
193, 193, 193, 193,
];
assert_eq!(
bincode::serialize(&sample_app_messages()).unwrap(),
APP_MESSAGES,
"ApplicationMessages (tag 0) encoding changed"
);
assert_eq!(
bincode::serialize(&sample_register()).unwrap(),
REGISTER,
"RegisterDelegate (tag 1) encoding changed"
);
assert_eq!(
bincode::serialize(&sample_unregister()).unwrap(),
UNREGISTER,
"UnregisterDelegate (tag 2) encoding changed"
);
assert_eq!(
bincode::serialize(&sample_register_with_predecessors()).unwrap(),
REGISTER_WITH_PREDECESSORS,
"RegisterDelegateWithPredecessors (tag 3) encoding changed"
);
assert_eq!(APP_MESSAGES[..4], 0u32.to_le_bytes());
assert_eq!(REGISTER[..4], 1u32.to_le_bytes());
assert_eq!(UNREGISTER[..4], 2u32.to_le_bytes());
assert_eq!(REGISTER_WITH_PREDECESSORS[..4], 3u32.to_le_bytes());
assert!(matches!(
bincode::deserialize::<DelegateRequest>(APP_MESSAGES).unwrap(),
DelegateRequest::ApplicationMessages { .. }
));
assert!(matches!(
bincode::deserialize::<DelegateRequest>(REGISTER).unwrap(),
DelegateRequest::RegisterDelegate { .. }
));
assert!(matches!(
bincode::deserialize::<DelegateRequest>(UNREGISTER).unwrap(),
DelegateRequest::UnregisterDelegate(_)
));
assert!(matches!(
bincode::deserialize::<DelegateRequest>(REGISTER_WITH_PREDECESSORS).unwrap(),
DelegateRequest::RegisterDelegateWithPredecessors { .. }
));
}
#[test]
fn register_with_predecessors_tag_is_three() {
let req = DelegateRequest::RegisterDelegateWithPredecessors {
delegate: sample_container(),
cipher: [0; 32],
nonce: [0; 24],
predecessors: vec![],
};
assert_eq!(
bincode::serialize(&req).unwrap()[..4],
3u32.to_le_bytes(),
"RegisterDelegateWithPredecessors must be the 4th variant (tag 3)"
);
}
#[test]
fn register_with_predecessors_round_trips() {
let predecessors = vec![sample_key(0xA0), sample_key(0xB0), sample_key(0xC0)];
let delegate = sample_container();
let expected_key = delegate.key().clone();
let req = DelegateRequest::RegisterDelegateWithPredecessors {
delegate,
cipher: [0x33; 32],
nonce: [0x44; 24],
predecessors: predecessors.clone(),
};
let bytes = bincode::serialize(&req).unwrap();
let decoded: DelegateRequest = bincode::deserialize(&bytes).unwrap();
match decoded {
DelegateRequest::RegisterDelegateWithPredecessors {
delegate,
cipher,
nonce,
predecessors: got,
} => {
assert_eq!(delegate.key(), &expected_key, "delegate preserved");
assert_eq!(cipher, [0x33; 32], "cipher preserved");
assert_eq!(nonce, [0x44; 24], "nonce preserved");
assert_eq!(
got, predecessors,
"full predecessor list preserved in order"
);
}
other => panic!("round-trip produced the wrong variant: {other:?}"),
}
}
#[test]
fn key_returns_the_new_delegate() {
let delegate = sample_container();
let expected_key = delegate.key().clone();
let req = DelegateRequest::RegisterDelegateWithPredecessors {
delegate,
cipher: [0; 32],
nonce: [0; 24],
predecessors: vec![sample_key(0xA0)],
};
assert_eq!(req.key(), &expected_key);
}
#[test]
fn fbs_decode_rejects_unknown_discriminant() {
use crate::client_api::TryFromFbs;
use crate::generated::client_request::{
finish_client_request_buffer, root_as_client_request,
ClientRequest as FbsClientRequest, ClientRequestArgs, ClientRequestType,
DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateRequest as FbsDelegateRequest,
DelegateRequestArgs, DelegateRequestType, UnregisterDelegate, UnregisterDelegateArgs,
};
let mut b = flatbuffers::FlatBufferBuilder::new();
let key = b.create_vector(&[0u8; 32]);
let code_hash = b.create_vector(&[0u8; 32]);
let dk = FbsDelegateKey::create(
&mut b,
&DelegateKeyArgs {
key: Some(key),
code_hash: Some(code_hash),
},
);
let unreg = UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
let dreq = FbsDelegateRequest::create(
&mut b,
&DelegateRequestArgs {
delegate_request_type: DelegateRequestType(99),
delegate_request: Some(unreg.as_union_value()),
},
);
let creq = FbsClientRequest::create(
&mut b,
&ClientRequestArgs {
client_request_type: ClientRequestType::DelegateRequest,
client_request: Some(dreq.as_union_value()),
},
);
finish_client_request_buffer(&mut b, creq);
let bytes = b.finished_data().to_vec();
let client =
root_as_client_request(&bytes).expect("verifier accepts an unknown union discriminant");
let fbs_delegate = client
.client_request_as_delegate_request()
.expect("client_request is a DelegateRequest");
let decoded = DelegateRequest::try_decode_fbs(&fbs_delegate);
assert!(
decoded.is_err(),
"an unknown DelegateRequestType discriminant must be a clean \
per-request error, never a panic that downs the connection handler"
);
}
}
#[cfg(test)]
mod fbs_decode_hardening {
use super::{ClientRequest, ContractRequest};
use crate::client_api::TryFromFbs;
use crate::contract_interface::UpdateData;
use crate::generated::client_request::{
finish_client_request_buffer, ApplicationMessages, ApplicationMessagesArgs,
ClientRequest as FbsClientRequest, ClientRequestArgs, ClientRequestType,
ContractRequest as FbsContractRequest, ContractRequestArgs, ContractRequestType,
DelegateCode as FbsDelegateCode, DelegateCodeArgs,
DelegateContainer as FbsDelegateContainer, DelegateContainerArgs,
DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateRequest as FbsDelegateRequest,
DelegateRequestArgs, DelegateRequestType, DelegateType, Get as FbsGet, GetArgs,
InboundDelegateMsg as FbsInboundDelegateMsg, InboundDelegateMsgArgs,
InboundDelegateMsgType, Put as FbsPut, PutArgs, RegisterDelegate, RegisterDelegateArgs,
RelatedContract, RelatedContractArgs, RelatedContracts as FbsRelatedContracts,
RelatedContractsArgs, Update as FbsUpdate, UpdateArgs, WasmDelegateV1, WasmDelegateV1Args,
};
use crate::generated::common::{
ApplicationMessage as FbsApplicationMessage, ApplicationMessageArgs,
ContractCode as FbsContractCode, ContractCodeArgs,
ContractContainer as FbsContractContainer, ContractContainerArgs,
ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
ContractKey as FbsContractKey, ContractKeyArgs, ContractType, RelatedDeltaUpdate,
RelatedDeltaUpdateArgs, RelatedStateAndDeltaUpdate, RelatedStateAndDeltaUpdateArgs,
RelatedStateUpdate, RelatedStateUpdateArgs, StateUpdate, StateUpdateArgs,
UpdateData as FbsUpdateData, UpdateDataArgs, UpdateDataType, WasmContractV1,
WasmContractV1Args,
};
type Builder<'a> = flatbuffers::FlatBufferBuilder<'a>;
const INSTANCE: [u8; 32] = [
0x00, 0xff, 0x7a, 0x01, 0x30, 0x4f, 0x49, 0x6c, 0x2b, 0x2f, 0x5c, 0x7f, 0x80, 0xfe, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21,
];
const CODE_HASH: [u8; 32] = [42u8; 32];
fn instance_offset<'a>(
b: &mut Builder<'a>,
bytes: &[u8],
) -> flatbuffers::WIPOffset<FbsContractInstanceId<'a>> {
let data = b.create_vector(bytes);
FbsContractInstanceId::create(b, &ContractInstanceIdArgs { data: Some(data) })
}
fn key_offset<'a>(
b: &mut Builder<'a>,
instance: &[u8],
code: &[u8],
) -> flatbuffers::WIPOffset<FbsContractKey<'a>> {
let instance = instance_offset(b, instance);
let code = b.create_vector(code);
FbsContractKey::create(
b,
&ContractKeyArgs {
instance: Some(instance),
code: Some(code),
},
)
}
fn delegate_key_offset<'a>(
b: &mut Builder<'a>,
key: &[u8],
code_hash: &[u8],
) -> flatbuffers::WIPOffset<FbsDelegateKey<'a>> {
let key = b.create_vector(key);
let code_hash = b.create_vector(code_hash);
FbsDelegateKey::create(
b,
&DelegateKeyArgs {
key: Some(key),
code_hash: Some(code_hash),
},
)
}
fn finish_contract(
b: &mut Builder<'_>,
ty: ContractRequestType,
req: flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
) -> Vec<u8> {
let contract = FbsContractRequest::create(
b,
&ContractRequestArgs {
contract_request_type: ty,
contract_request: Some(req),
},
);
let client = FbsClientRequest::create(
b,
&ClientRequestArgs {
client_request_type: ClientRequestType::ContractRequest,
client_request: Some(contract.as_union_value()),
},
);
finish_client_request_buffer(b, client);
b.finished_data().to_vec()
}
fn finish_delegate(
b: &mut Builder<'_>,
ty: DelegateRequestType,
req: flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
) -> Vec<u8> {
let delegate = FbsDelegateRequest::create(
b,
&DelegateRequestArgs {
delegate_request_type: ty,
delegate_request: Some(req),
},
);
let client = FbsClientRequest::create(
b,
&ClientRequestArgs {
client_request_type: ClientRequestType::DelegateRequest,
client_request: Some(delegate.as_union_value()),
},
);
finish_client_request_buffer(b, client);
b.finished_data().to_vec()
}
fn client_request_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
let get = FbsGet::create(
&mut b,
&GetArgs {
key: Some(key),
fetch_contract: false,
subscribe: false,
blocking_subscribe: false,
},
);
let contract = FbsContractRequest::create(
&mut b,
&ContractRequestArgs {
contract_request_type: ContractRequestType::Get,
contract_request: Some(get.as_union_value()),
},
);
let client = FbsClientRequest::create(
&mut b,
&ClientRequestArgs {
client_request_type: ClientRequestType(d),
client_request: Some(contract.as_union_value()),
},
);
finish_client_request_buffer(&mut b, client);
b.finished_data().to_vec()
}
fn contract_request_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
let get = FbsGet::create(
&mut b,
&GetArgs {
key: Some(key),
fetch_contract: false,
subscribe: false,
blocking_subscribe: false,
},
);
finish_contract(&mut b, ContractRequestType(d), get.as_union_value())
}
fn delegate_request_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
let params = b.create_vector(&[1u8, 2, 3]);
let payload = b.create_vector(&[9u8; 4]);
let context = b.create_vector(&[0u8; 2]);
let app = FbsApplicationMessage::create(
&mut b,
&ApplicationMessageArgs {
payload: Some(payload),
context: Some(context),
processed: false,
},
);
let inbound_msg = FbsInboundDelegateMsg::create(
&mut b,
&InboundDelegateMsgArgs {
inbound_type: InboundDelegateMsgType::common_ApplicationMessage,
inbound: Some(app.as_union_value()),
},
);
let inbound = b.create_vector(&[inbound_msg]);
let msgs = ApplicationMessages::create(
&mut b,
&ApplicationMessagesArgs {
key: Some(dk),
params: Some(params),
inbound: Some(inbound),
},
);
finish_delegate(&mut b, DelegateRequestType(d), msgs.as_union_value())
}
fn contract_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let code_data = b.create_vector(&[0u8; 8]);
let code_hash = b.create_vector(&CODE_HASH);
let code = FbsContractCode::create(
&mut b,
&ContractCodeArgs {
data: Some(code_data),
code_hash: Some(code_hash),
},
);
let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
let params = b.create_vector(&[1u8, 2]);
let wasm = WasmContractV1::create(
&mut b,
&WasmContractV1Args {
data: Some(code),
parameters: Some(params),
key: Some(key),
},
);
let container = FbsContractContainer::create(
&mut b,
&ContractContainerArgs {
contract_type: ContractType(d),
contract: Some(wasm.as_union_value()),
},
);
let state = b.create_vector(&[3u8; 4]);
let empty: Vec<flatbuffers::WIPOffset<RelatedContract>> = vec![];
let contracts = b.create_vector(&empty);
let related = FbsRelatedContracts::create(
&mut b,
&RelatedContractsArgs {
contracts: Some(contracts),
},
);
let put = FbsPut::create(
&mut b,
&PutArgs {
container: Some(container),
wrapped_state: Some(state),
related_contracts: Some(related),
subscribe: false,
blocking_subscribe: false,
},
);
finish_contract(&mut b, ContractRequestType::Put, put.as_union_value())
}
fn delegate_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let code_data = b.create_vector(&[0u8; 8]);
let code_hash = b.create_vector(&CODE_HASH);
let code = FbsDelegateCode::create(
&mut b,
&DelegateCodeArgs {
data: Some(code_data),
code_hash: Some(code_hash),
},
);
let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
let params = b.create_vector(&[1u8, 2]);
let wasm = WasmDelegateV1::create(
&mut b,
&WasmDelegateV1Args {
parameters: Some(params),
data: Some(code),
key: Some(dk),
},
);
let container = FbsDelegateContainer::create(
&mut b,
&DelegateContainerArgs {
delegate_type: DelegateType(d),
delegate: Some(wasm.as_union_value()),
},
);
let cipher = b.create_vector(&[1u8; 32]);
let nonce = b.create_vector(&[2u8; 24]);
let register = RegisterDelegate::create(
&mut b,
&RegisterDelegateArgs {
delegate: Some(container),
cipher: Some(cipher),
nonce: Some(nonce),
},
);
finish_delegate(
&mut b,
DelegateRequestType::RegisterDelegate,
register.as_union_value(),
)
}
fn update_data_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let state = b.create_vector(&[5u8; 4]);
let state_update = StateUpdate::create(&mut b, &StateUpdateArgs { state: Some(state) });
let data = FbsUpdateData::create(
&mut b,
&UpdateDataArgs {
update_data_type: UpdateDataType(d),
update_data: Some(state_update.as_union_value()),
},
);
let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
let update = FbsUpdate::create(
&mut b,
&UpdateArgs {
key: Some(key),
data: Some(data),
},
);
finish_contract(&mut b, ContractRequestType::Update, update.as_union_value())
}
fn inbound_delegate_msg_type(d: u8, force_defaults: bool) -> Vec<u8> {
let mut b = Builder::new();
b.force_defaults(force_defaults);
let payload = b.create_vector(&[9u8; 4]);
let context = b.create_vector(&[0u8; 2]);
let app = FbsApplicationMessage::create(
&mut b,
&ApplicationMessageArgs {
payload: Some(payload),
context: Some(context),
processed: false,
},
);
let inbound_msg = FbsInboundDelegateMsg::create(
&mut b,
&InboundDelegateMsgArgs {
inbound_type: InboundDelegateMsgType(d),
inbound: Some(app.as_union_value()),
},
);
let inbound = b.create_vector(&[inbound_msg]);
let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
let params = b.create_vector(&[1u8, 2, 3]);
let msgs = ApplicationMessages::create(
&mut b,
&ApplicationMessagesArgs {
key: Some(dk),
params: Some(params),
inbound: Some(inbound),
},
);
finish_delegate(
&mut b,
DelegateRequestType::ApplicationMessages,
msgs.as_union_value(),
)
}
type UnionCase = (&'static str, fn(u8, bool) -> Vec<u8>, u8);
#[test]
fn no_union_discriminant_panics_the_decoder() {
let cases: [UnionCase; 7] = [
("ClientRequestType", client_request_type, 1),
("ContractRequestType", contract_request_type, 3),
("DelegateRequestType", delegate_request_type, 1),
("ContractType", contract_type, 1),
("DelegateType", delegate_type, 1),
("UpdateDataType", update_data_type, 1),
(
"InboundDelegateMsgType",
inbound_delegate_msg_type,
InboundDelegateMsgType::common_ApplicationMessage.0,
),
];
for (union, build, valid) in cases {
for d in 0..=u8::MAX {
let bytes = build(d, false);
let _ = ClientRequest::try_decode_fbs(&bytes);
}
let out_of_range = build(200, false);
let err = ClientRequest::try_decode_fbs(&out_of_range)
.expect_err("{union}: an out-of-range discriminant must be a clean error");
assert_eq!(
err.to_string(),
format!(
"Failed decoding message from client request: unknown {union} \
discriminant: 200"
),
"{union}: the error must come from the decoder's union arm, not \
from the verifier — otherwise this sweep pins nothing"
);
let none = build(0, true);
let err = ClientRequest::try_decode_fbs(&none)
.expect_err("{union}: a NONE discriminant must be a clean error");
assert_eq!(
err.to_string(),
format!(
"Failed decoding message from client request: unknown {union} \
discriminant: 0"
),
"{union}: an explicit NONE must reach the decoder's union arm"
);
let good = build(valid, false);
assert!(
ClientRequest::try_decode_fbs(&good).is_ok(),
"{union}: the real discriminant must still decode; the guard \
must not break the happy path"
);
}
}
fn update_with_related(variant: UpdateDataType, id: &[u8]) -> Vec<u8> {
let mut b = Builder::new();
let related_to = instance_offset(&mut b, id);
let payload = b.create_vector(&[5u8; 4]);
let data_offset = match variant {
UpdateDataType::RelatedStateUpdate => RelatedStateUpdate::create(
&mut b,
&RelatedStateUpdateArgs {
related_to: Some(related_to),
state: Some(payload),
},
)
.as_union_value(),
UpdateDataType::RelatedDeltaUpdate => RelatedDeltaUpdate::create(
&mut b,
&RelatedDeltaUpdateArgs {
related_to: Some(related_to),
delta: Some(payload),
},
)
.as_union_value(),
UpdateDataType::RelatedStateAndDeltaUpdate => {
let delta = b.create_vector(&[6u8; 4]);
RelatedStateAndDeltaUpdate::create(
&mut b,
&RelatedStateAndDeltaUpdateArgs {
related_to: Some(related_to),
state: Some(payload),
delta: Some(delta),
},
)
.as_union_value()
}
other => panic!("not a related update variant: {}", other.0),
};
let data = FbsUpdateData::create(
&mut b,
&UpdateDataArgs {
update_data_type: variant,
update_data: Some(data_offset),
},
);
let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
let update = FbsUpdate::create(
&mut b,
&UpdateArgs {
key: Some(key),
data: Some(data),
},
);
finish_contract(&mut b, ContractRequestType::Update, update.as_union_value())
}
fn decoded_related_to(bytes: &[u8]) -> [u8; 32] {
let req = ClientRequest::try_decode_fbs(bytes)
.expect("a well-formed related update must decode, not panic");
let ClientRequest::ContractOp(ContractRequest::Update { data, .. }) = req else {
panic!("expected an UPDATE, got {req:?}");
};
match data {
UpdateData::RelatedState { related_to, .. }
| UpdateData::RelatedDelta { related_to, .. }
| UpdateData::RelatedStateAndDelta { related_to, .. } => *related_to,
other => panic!("expected a related update, got {other:?}"),
}
}
#[test]
fn related_state_update_round_trips_the_raw_instance_id() {
let bytes = update_with_related(UpdateDataType::RelatedStateUpdate, &INSTANCE);
assert_eq!(decoded_related_to(&bytes), INSTANCE);
}
#[test]
fn related_delta_update_round_trips_the_raw_instance_id() {
let bytes = update_with_related(UpdateDataType::RelatedDeltaUpdate, &INSTANCE);
assert_eq!(decoded_related_to(&bytes), INSTANCE);
}
#[test]
fn related_state_and_delta_update_round_trips_the_raw_instance_id() {
let bytes = update_with_related(UpdateDataType::RelatedStateAndDeltaUpdate, &INSTANCE);
assert_eq!(decoded_related_to(&bytes), INSTANCE);
}
#[test]
fn related_to_wrong_length_is_rejected() {
for (variant, field) in [
(
UpdateDataType::RelatedStateUpdate,
"RelatedStateUpdate.related_to.data",
),
(
UpdateDataType::RelatedDeltaUpdate,
"RelatedDeltaUpdate.related_to.data",
),
(
UpdateDataType::RelatedStateAndDeltaUpdate,
"RelatedStateAndDeltaUpdate.related_to.data",
),
] {
let bytes = update_with_related(variant, &[1u8; 8]);
let err = ClientRequest::try_decode_fbs(&bytes)
.expect_err("an 8-byte related_to must be rejected");
let msg = err.to_string();
assert!(
msg.contains(field) && msg.contains("got 8 bytes"),
"the error must name {field} and the observed length, got: {msg}"
);
}
}
fn put_with_related_contract(id: &[u8]) -> Vec<u8> {
let mut b = Builder::new();
let code_data = b.create_vector(&[0u8; 8]);
let code_hash = b.create_vector(&CODE_HASH);
let code = FbsContractCode::create(
&mut b,
&ContractCodeArgs {
data: Some(code_data),
code_hash: Some(code_hash),
},
);
let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
let params = b.create_vector(&[1u8, 2]);
let wasm = WasmContractV1::create(
&mut b,
&WasmContractV1Args {
data: Some(code),
parameters: Some(params),
key: Some(key),
},
);
let container = FbsContractContainer::create(
&mut b,
&ContractContainerArgs {
contract_type: ContractType::WasmContractV1,
contract: Some(wasm.as_union_value()),
},
);
let related_id = instance_offset(&mut b, id);
let related_state = b.create_vector(&[8u8; 3]);
let related_contract = RelatedContract::create(
&mut b,
&RelatedContractArgs {
instance_id: Some(related_id),
state: Some(related_state),
},
);
let contracts = b.create_vector(&[related_contract]);
let related = FbsRelatedContracts::create(
&mut b,
&RelatedContractsArgs {
contracts: Some(contracts),
},
);
let state = b.create_vector(&[3u8; 4]);
let put = FbsPut::create(
&mut b,
&PutArgs {
container: Some(container),
wrapped_state: Some(state),
related_contracts: Some(related),
subscribe: false,
blocking_subscribe: false,
},
);
finish_contract(&mut b, ContractRequestType::Put, put.as_union_value())
}
#[test]
fn put_related_contract_round_trips_the_raw_instance_id() {
let bytes = put_with_related_contract(&INSTANCE);
let req = ClientRequest::try_decode_fbs(&bytes)
.expect("a PUT carrying a related contract must decode, not panic");
let ClientRequest::ContractOp(ContractRequest::Put {
related_contracts, ..
}) = req
else {
panic!("expected a PUT, got {req:?}");
};
let ids: Vec<[u8; 32]> = related_contracts
.into_owned()
.states()
.map(|(id, _)| **id)
.collect();
assert_eq!(
ids,
vec![INSTANCE],
"the related contract id must round-trip"
);
}
#[test]
fn put_related_contract_wrong_length_id_is_rejected() {
let bytes = put_with_related_contract(&[1u8; 8]);
let err = ClientRequest::try_decode_fbs(&bytes)
.expect_err("an 8-byte related contract id must be rejected");
let msg = err.to_string();
assert!(
msg.contains("RelatedContract.instance_id") && msg.contains("got 8 bytes"),
"got: {msg}"
);
}
fn unregister_delegate(key_len: usize) -> Vec<u8> {
use crate::generated::client_request::{UnregisterDelegate, UnregisterDelegateArgs};
let mut b = Builder::new();
let dk = delegate_key_offset(&mut b, &vec![7u8; key_len], &CODE_HASH);
let unregister =
UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
finish_delegate(
&mut b,
DelegateRequestType::UnregisterDelegate,
unregister.as_union_value(),
)
}
#[test]
fn delegate_key_wrong_length_is_rejected_not_panicking() {
let short = unregister_delegate(8);
let err = ClientRequest::try_decode_fbs(&short)
.expect_err("an 8-byte delegate key must be rejected");
let msg = err.to_string();
assert!(
msg.contains("DelegateKey.key") && msg.contains("got 8 bytes"),
"got: {msg}"
);
let long = unregister_delegate(64);
let err = ClientRequest::try_decode_fbs(&long)
.expect_err("a 64-byte delegate key must be rejected");
assert!(err.to_string().contains("got 64 bytes"), "got: {err}");
let good = unregister_delegate(32);
assert!(
ClientRequest::try_decode_fbs(&good).is_ok(),
"a 32-byte delegate key must still decode"
);
}
fn register_delegate(cipher_len: usize, nonce_len: usize) -> Vec<u8> {
let mut b = Builder::new();
let code_data = b.create_vector(&[0u8; 8]);
let code_hash = b.create_vector(&CODE_HASH);
let code = FbsDelegateCode::create(
&mut b,
&DelegateCodeArgs {
data: Some(code_data),
code_hash: Some(code_hash),
},
);
let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
let params = b.create_vector(&[1u8, 2]);
let wasm = WasmDelegateV1::create(
&mut b,
&WasmDelegateV1Args {
parameters: Some(params),
data: Some(code),
key: Some(dk),
},
);
let container = FbsDelegateContainer::create(
&mut b,
&DelegateContainerArgs {
delegate_type: DelegateType::WasmDelegateV1,
delegate: Some(wasm.as_union_value()),
},
);
let cipher = b.create_vector(&vec![1u8; cipher_len]);
let nonce = b.create_vector(&vec![2u8; nonce_len]);
let register = RegisterDelegate::create(
&mut b,
&RegisterDelegateArgs {
delegate: Some(container),
cipher: Some(cipher),
nonce: Some(nonce),
},
);
finish_delegate(
&mut b,
DelegateRequestType::RegisterDelegate,
register.as_union_value(),
)
}
#[test]
fn register_delegate_wrong_length_cipher_or_nonce_is_rejected() {
let err = ClientRequest::try_decode_fbs(®ister_delegate(16, 24))
.expect_err("a 16-byte cipher must be rejected");
let msg = err.to_string();
assert!(
msg.contains("RegisterDelegate.cipher") && msg.contains("got 16 bytes"),
"got: {msg}"
);
let err = ClientRequest::try_decode_fbs(®ister_delegate(32, 8))
.expect_err("an 8-byte nonce must be rejected");
let msg = err.to_string();
assert!(
msg.contains("RegisterDelegate.nonce") && msg.contains("got 8 bytes"),
"got: {msg}"
);
assert!(
ClientRequest::try_decode_fbs(®ister_delegate(32, 24)).is_ok(),
"correct cipher/nonce lengths must still decode"
);
}
#[test]
fn host_response_encodes_related_to_as_raw_bytes() {
use crate::client_api::{ContractResponse, HostResponse};
use crate::contract_interface::{ContractInstanceId, ContractKey, State};
use crate::generated::host_response::{root_as_host_response, ContractResponseType};
let related = ContractInstanceId::new(INSTANCE);
let key = ContractKey::from_params_and_code(
crate::parameters::Parameters::from(vec![1u8, 2]),
crate::contract_interface::ContractCode::from(vec![0u8; 8]),
);
let response = HostResponse::ContractResponse(ContractResponse::UpdateNotification {
key,
update: UpdateData::RelatedState {
related_to: related,
state: State::from(vec![9u8; 4]),
},
});
let bytes = response.into_fbs_bytes().expect("encoding must succeed");
let host = root_as_host_response(&bytes).expect("the encoder must emit a valid buffer");
let contract = host
.response_as_contract_response()
.expect("a ContractResponse");
assert_eq!(
contract.contract_response_type(),
ContractResponseType::UpdateNotification
);
let notification = contract
.contract_response_as_update_notification()
.expect("an UpdateNotification");
let related_update = notification
.update()
.update_data_as_related_state_update()
.expect("a RelatedStateUpdate");
assert_eq!(
related_update.related_to().data().bytes(),
&INSTANCE,
"related_to must be the 32 RAW id bytes. Encoding it as base58 text \
puts ~44 ASCII bytes in a field the TypeScript SDK reads as a raw \
Uint8Array, and that our own decoder now rejects."
);
}
#[test]
fn secrets_id_wrong_length_hash_is_rejected_not_panicking() {
use crate::delegate_interface::SecretsId;
use crate::generated::common::{SecretsId as FbsSecretsId, SecretsIdArgs};
let build = |hash_len: usize| {
let mut b = Builder::new();
let key = b.create_vector(&[1u8, 2, 3]);
let hash = b.create_vector(&vec![4u8; hash_len]);
let id = FbsSecretsId::create(
&mut b,
&SecretsIdArgs {
key: Some(key),
hash: Some(hash),
},
);
b.finish_minimal(id);
b.finished_data().to_vec()
};
let bytes = build(8);
let fbs = flatbuffers::root::<FbsSecretsId>(&bytes)
.expect("the verifier accepts a short required vector");
let err = SecretsId::try_decode_fbs(&fbs).expect_err("an 8-byte hash must be rejected");
assert!(
err.to_string().contains("SecretsId.hash") && err.to_string().contains("got 8 bytes"),
"got: {err}"
);
let bytes = build(32);
let fbs = flatbuffers::root::<FbsSecretsId>(&bytes).expect("well-formed");
assert!(
SecretsId::try_decode_fbs(&fbs).is_ok(),
"a 32-byte hash must still decode"
);
}
}