use std::{
borrow::{Borrow, Cow},
fmt::Display,
fs::File,
io::Read,
ops::Deref,
path::Path,
};
use blake3::{traits::digest::Digest, Hasher as Blake3};
use serde::{Deserialize, Deserializer, Serialize};
use serde_with::serde_as;
use crate::generated::client_request::{
DelegateKey as FbsDelegateKey, InboundDelegateMsg as FbsInboundDelegateMsg,
InboundDelegateMsgType,
};
use crate::common_generated::common::SecretsId as FbsSecretsId;
use crate::client_api::{fixed_size_field, unknown_union_discriminant, TryFromFbs, WsApiError};
use crate::contract_interface::{RelatedContracts, UpdateData, CONTRACT_KEY_SIZE};
use crate::prelude::{ContractInstanceId, WrappedState};
use crate::versioning::ContractContainer;
use crate::{code_hash::CodeHash, prelude::Parameters};
const DELEGATE_HASH_LENGTH: usize = 32;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Delegate<'a> {
#[serde(borrow)]
parameters: Parameters<'a>,
#[serde(borrow)]
pub data: DelegateCode<'a>,
key: DelegateKey,
}
impl Delegate<'_> {
pub fn key(&self) -> &DelegateKey {
&self.key
}
pub fn code(&self) -> &DelegateCode<'_> {
&self.data
}
pub fn code_hash(&self) -> &CodeHash {
&self.data.code_hash
}
pub fn params(&self) -> &Parameters<'_> {
&self.parameters
}
pub fn into_owned(self) -> Delegate<'static> {
Delegate {
parameters: self.parameters.into_owned(),
data: self.data.into_owned(),
key: self.key,
}
}
pub fn size(&self) -> usize {
self.parameters.size() + self.data.size()
}
pub(crate) fn deserialize_delegate<'de, D>(deser: D) -> Result<Delegate<'static>, D::Error>
where
D: Deserializer<'de>,
{
let data: Delegate<'de> = Deserialize::deserialize(deser)?;
Ok(data.into_owned())
}
}
impl PartialEq for Delegate<'_> {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
}
}
impl Eq for Delegate<'_> {}
impl<'a> From<(&DelegateCode<'a>, &Parameters<'a>)> for Delegate<'a> {
fn from((data, parameters): (&DelegateCode<'a>, &Parameters<'a>)) -> Self {
Self {
key: DelegateKey::from_params_and_code(parameters, data),
parameters: parameters.clone(),
data: data.clone(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde_as]
pub struct DelegateCode<'a> {
#[serde_as(as = "serde_with::Bytes")]
#[serde(borrow)]
pub(crate) data: Cow<'a, [u8]>,
pub(crate) code_hash: CodeHash,
}
impl DelegateCode<'static> {
pub fn load_raw(path: &Path) -> Result<Self, std::io::Error> {
let contract_data = Self::load_bytes(path)?;
Ok(DelegateCode::from(contract_data))
}
pub(crate) fn load_bytes(path: &Path) -> Result<Vec<u8>, std::io::Error> {
let mut contract_file = File::open(path)?;
let mut contract_data = if let Ok(md) = contract_file.metadata() {
Vec::with_capacity(md.len() as usize)
} else {
Vec::new()
};
contract_file.read_to_end(&mut contract_data)?;
Ok(contract_data)
}
}
impl DelegateCode<'_> {
pub fn hash(&self) -> &CodeHash {
&self.code_hash
}
pub fn hash_str(&self) -> String {
Self::encode_hash(&self.code_hash.0)
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn encode_hash(hash: &[u8; DELEGATE_HASH_LENGTH]) -> String {
bs58::encode(hash)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_string()
}
pub fn into_owned(self) -> DelegateCode<'static> {
DelegateCode {
code_hash: self.code_hash,
data: Cow::from(self.data.into_owned()),
}
}
pub fn size(&self) -> usize {
self.data.len()
}
}
impl PartialEq for DelegateCode<'_> {
fn eq(&self, other: &Self) -> bool {
self.code_hash == other.code_hash
}
}
impl Eq for DelegateCode<'_> {}
impl AsRef<[u8]> for DelegateCode<'_> {
fn as_ref(&self) -> &[u8] {
self.data.borrow()
}
}
impl From<Vec<u8>> for DelegateCode<'static> {
fn from(data: Vec<u8>) -> Self {
let key = CodeHash::from_code(data.as_slice());
DelegateCode {
data: Cow::from(data),
code_hash: key,
}
}
}
impl<'a> From<&'a [u8]> for DelegateCode<'a> {
fn from(code: &'a [u8]) -> Self {
let key = CodeHash::from_code(code);
DelegateCode {
data: Cow::from(code),
code_hash: key,
}
}
}
#[serde_as]
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub struct DelegateKey {
#[serde_as(as = "[_; DELEGATE_HASH_LENGTH]")]
key: [u8; DELEGATE_HASH_LENGTH],
code_hash: CodeHash,
}
impl From<DelegateKey> for SecretsId {
fn from(key: DelegateKey) -> SecretsId {
SecretsId {
hash: key.key,
key: vec![],
}
}
}
impl DelegateKey {
pub const fn new(key: [u8; DELEGATE_HASH_LENGTH], code_hash: CodeHash) -> Self {
Self { key, code_hash }
}
fn from_params_and_code<'a>(
params: impl Borrow<Parameters<'a>>,
wasm_code: impl Borrow<DelegateCode<'a>>,
) -> Self {
let code = wasm_code.borrow();
let key = generate_id(params.borrow(), code);
Self {
key,
code_hash: *code.hash(),
}
}
pub fn encode(&self) -> String {
bs58::encode(self.key)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_string()
}
pub fn code_hash(&self) -> &CodeHash {
&self.code_hash
}
pub fn bytes(&self) -> &[u8] {
self.key.as_ref()
}
pub fn from_params(
code_hash: impl Into<String>,
parameters: &Parameters,
) -> Result<Self, bs58::decode::Error> {
let mut code_key = [0; DELEGATE_HASH_LENGTH];
bs58::decode(code_hash.into())
.with_alphabet(bs58::Alphabet::BITCOIN)
.onto(&mut code_key)?;
let mut hasher = Blake3::new();
hasher.update(code_key.as_slice());
hasher.update(parameters.as_ref());
let full_key_arr = hasher.finalize();
debug_assert_eq!(full_key_arr[..].len(), DELEGATE_HASH_LENGTH);
let mut key = [0; DELEGATE_HASH_LENGTH];
key.copy_from_slice(&full_key_arr);
Ok(Self {
key,
code_hash: CodeHash(code_key),
})
}
}
impl Deref for DelegateKey {
type Target = [u8; DELEGATE_HASH_LENGTH];
fn deref(&self) -> &Self::Target {
&self.key
}
}
impl Display for DelegateKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encode())
}
}
impl<'a> TryFromFbs<&FbsDelegateKey<'a>> for DelegateKey {
fn try_decode_fbs(key: &FbsDelegateKey<'a>) -> Result<Self, WsApiError> {
let key_bytes =
fixed_size_field::<DELEGATE_HASH_LENGTH>("DelegateKey.key", key.key().bytes())?;
let code_hash = CodeHash::new(fixed_size_field::<CONTRACT_KEY_SIZE>(
"DelegateKey.code_hash",
key.code_hash().bytes(),
)?);
Ok(DelegateKey {
key: key_bytes,
code_hash,
})
}
}
#[non_exhaustive]
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
pub enum DelegateError {
#[error("de/serialization error: {0}")]
Deser(String),
#[error("{0}")]
Other(String),
}
fn generate_id<'a>(
parameters: &Parameters<'a>,
code_data: &DelegateCode<'a>,
) -> [u8; DELEGATE_HASH_LENGTH] {
let contract_hash = code_data.hash();
let mut hasher = Blake3::new();
hasher.update(contract_hash.0.as_slice());
hasher.update(parameters.as_ref());
let full_key_arr = hasher.finalize();
debug_assert_eq!(full_key_arr[..].len(), DELEGATE_HASH_LENGTH);
let mut key = [0; DELEGATE_HASH_LENGTH];
key.copy_from_slice(&full_key_arr);
key
}
#[serde_as]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct SecretsId {
#[serde_as(as = "serde_with::Bytes")]
key: Vec<u8>,
#[serde_as(as = "[_; 32]")]
hash: [u8; 32],
}
impl SecretsId {
pub fn new(key: Vec<u8>) -> Self {
let mut hasher = Blake3::new();
hasher.update(&key);
let hashed = hasher.finalize();
let mut hash = [0; 32];
hash.copy_from_slice(&hashed);
Self { key, hash }
}
pub fn encode(&self) -> String {
bs58::encode(self.hash)
.with_alphabet(bs58::Alphabet::BITCOIN)
.into_string()
}
pub fn hash(&self) -> &[u8; 32] {
&self.hash
}
pub fn key(&self) -> &[u8] {
self.key.as_slice()
}
}
impl Display for SecretsId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.encode())
}
}
impl<'a> TryFromFbs<&FbsSecretsId<'a>> for SecretsId {
fn try_decode_fbs(key: &FbsSecretsId<'a>) -> Result<Self, WsApiError> {
let key_hash = fixed_size_field::<32>("SecretsId.hash", key.hash().bytes())?;
Ok(SecretsId {
key: key.key().bytes().to_vec(),
hash: key_hash,
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum MessageOrigin {
WebApp(ContractInstanceId),
Delegate(DelegateKey),
}
pub trait DelegateInterface {
fn process(
ctx: &mut crate::delegate_host::DelegateCtx,
parameters: Parameters<'static>,
origin: Option<MessageOrigin>,
message: InboundDelegateMsg,
) -> Result<Vec<OutboundDelegateMsg>, DelegateError>;
}
#[serde_as]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct DelegateContext(#[serde_as(as = "serde_with::Bytes")] Vec<u8>);
impl DelegateContext {
pub const MAX_SIZE: usize = 4096 * 10 * 10;
pub fn new(bytes: Vec<u8>) -> Self {
assert!(bytes.len() < Self::MAX_SIZE);
Self(bytes)
}
pub fn append(&mut self, bytes: &mut Vec<u8>) {
assert!(self.0.len() + bytes.len() < Self::MAX_SIZE);
self.0.append(bytes)
}
pub fn replace(&mut self, bytes: Vec<u8>) {
assert!(bytes.len() < Self::MAX_SIZE);
let _ = std::mem::replace(&mut self.0, bytes);
}
}
impl AsRef<[u8]> for DelegateContext {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum InboundDelegateMsg<'a> {
ApplicationMessage(ApplicationMessage),
UserResponse(#[serde(borrow)] UserInputResponse<'a>),
GetContractResponse(GetContractResponse),
PutContractResponse(PutContractResponse),
UpdateContractResponse(UpdateContractResponse),
SubscribeContractResponse(SubscribeContractResponse),
ContractNotification(ContractNotification),
DelegateMessage(DelegateMessage),
UnsubscribeContractResponse(UnsubscribeContractResponse),
WakeupFired {
tag: Vec<u8>,
},
}
impl InboundDelegateMsg<'_> {
pub fn into_owned(self) -> InboundDelegateMsg<'static> {
match self {
InboundDelegateMsg::ApplicationMessage(r) => InboundDelegateMsg::ApplicationMessage(r),
InboundDelegateMsg::UserResponse(r) => InboundDelegateMsg::UserResponse(r.into_owned()),
InboundDelegateMsg::GetContractResponse(r) => {
InboundDelegateMsg::GetContractResponse(r)
}
InboundDelegateMsg::PutContractResponse(r) => {
InboundDelegateMsg::PutContractResponse(r)
}
InboundDelegateMsg::UpdateContractResponse(r) => {
InboundDelegateMsg::UpdateContractResponse(r)
}
InboundDelegateMsg::SubscribeContractResponse(r) => {
InboundDelegateMsg::SubscribeContractResponse(r)
}
InboundDelegateMsg::ContractNotification(r) => {
InboundDelegateMsg::ContractNotification(r)
}
InboundDelegateMsg::DelegateMessage(r) => InboundDelegateMsg::DelegateMessage(r),
InboundDelegateMsg::UnsubscribeContractResponse(r) => {
InboundDelegateMsg::UnsubscribeContractResponse(r)
}
InboundDelegateMsg::WakeupFired { tag } => InboundDelegateMsg::WakeupFired { tag },
}
}
pub fn get_context(&self) -> Option<&DelegateContext> {
match self {
InboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
Some(context)
}
InboundDelegateMsg::UserResponse(UserInputResponse { context, .. }) => Some(context),
InboundDelegateMsg::GetContractResponse(GetContractResponse { context, .. }) => {
Some(context)
}
InboundDelegateMsg::PutContractResponse(PutContractResponse { context, .. }) => {
Some(context)
}
InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
context, ..
}) => Some(context),
InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
context,
..
}) => Some(context),
InboundDelegateMsg::ContractNotification(ContractNotification { context, .. }) => {
Some(context)
}
InboundDelegateMsg::DelegateMessage(DelegateMessage { context, .. }) => Some(context),
InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
context,
..
}) => Some(context),
InboundDelegateMsg::WakeupFired { .. } => None,
}
}
pub fn get_mut_context(&mut self) -> Option<&mut DelegateContext> {
match self {
InboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
Some(context)
}
InboundDelegateMsg::UserResponse(UserInputResponse { context, .. }) => Some(context),
InboundDelegateMsg::GetContractResponse(GetContractResponse { context, .. }) => {
Some(context)
}
InboundDelegateMsg::PutContractResponse(PutContractResponse { context, .. }) => {
Some(context)
}
InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
context, ..
}) => Some(context),
InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
context,
..
}) => Some(context),
InboundDelegateMsg::ContractNotification(ContractNotification { context, .. }) => {
Some(context)
}
InboundDelegateMsg::DelegateMessage(DelegateMessage { context, .. }) => Some(context),
InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
context,
..
}) => Some(context),
InboundDelegateMsg::WakeupFired { .. } => None,
}
}
}
impl From<ApplicationMessage> for InboundDelegateMsg<'_> {
fn from(value: ApplicationMessage) -> Self {
Self::ApplicationMessage(value)
}
}
impl<'a> TryFromFbs<&FbsInboundDelegateMsg<'a>> for InboundDelegateMsg<'a> {
fn try_decode_fbs(msg: &FbsInboundDelegateMsg<'a>) -> Result<Self, WsApiError> {
match msg.inbound_type() {
InboundDelegateMsgType::common_ApplicationMessage => {
let app_msg = msg.inbound_as_common_application_message().unwrap();
let app_msg = ApplicationMessage {
payload: app_msg.payload().bytes().to_vec(),
context: DelegateContext::new(app_msg.context().bytes().to_vec()),
processed: app_msg.processed(),
};
Ok(InboundDelegateMsg::ApplicationMessage(app_msg))
}
InboundDelegateMsgType::UserInputResponse => {
let user_response = msg.inbound_as_user_input_response().unwrap();
let user_response = UserInputResponse {
request_id: user_response.request_id(),
response: ClientResponse::new(user_response.response().data().bytes().to_vec()),
context: DelegateContext::new(
user_response.delegate_context().bytes().to_vec(),
),
};
Ok(InboundDelegateMsg::UserResponse(user_response))
}
other => Err(unknown_union_discriminant(
"InboundDelegateMsgType",
other.0,
)),
}
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ApplicationMessage {
pub payload: Vec<u8>,
pub context: DelegateContext,
pub processed: bool,
}
impl ApplicationMessage {
pub fn new(payload: Vec<u8>) -> Self {
Self {
payload,
context: DelegateContext::default(),
processed: false,
}
}
pub fn with_context(mut self, context: DelegateContext) -> Self {
self.context = context;
self
}
pub fn processed(mut self, p: bool) -> Self {
self.processed = p;
self
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserInputResponse<'a> {
pub request_id: u32,
#[serde(borrow)]
pub response: ClientResponse<'a>,
pub context: DelegateContext,
}
impl UserInputResponse<'_> {
pub fn into_owned(self) -> UserInputResponse<'static> {
UserInputResponse {
request_id: self.request_id,
response: self.response.into_owned(),
context: self.context,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum OutboundDelegateMsg {
ApplicationMessage(ApplicationMessage),
RequestUserInput(
#[serde(deserialize_with = "OutboundDelegateMsg::deser_user_input_req")]
UserInputRequest<'static>,
),
ContextUpdated(DelegateContext),
GetContractRequest(GetContractRequest),
PutContractRequest(PutContractRequest),
UpdateContractRequest(UpdateContractRequest),
SubscribeContractRequest(SubscribeContractRequest),
SendDelegateMessage(DelegateMessage),
UnsubscribeContractRequest(UnsubscribeContractRequest),
}
impl From<ApplicationMessage> for OutboundDelegateMsg {
fn from(req: ApplicationMessage) -> Self {
Self::ApplicationMessage(req)
}
}
impl From<GetContractRequest> for OutboundDelegateMsg {
fn from(req: GetContractRequest) -> Self {
Self::GetContractRequest(req)
}
}
impl From<PutContractRequest> for OutboundDelegateMsg {
fn from(req: PutContractRequest) -> Self {
Self::PutContractRequest(req)
}
}
impl From<UpdateContractRequest> for OutboundDelegateMsg {
fn from(req: UpdateContractRequest) -> Self {
Self::UpdateContractRequest(req)
}
}
impl From<SubscribeContractRequest> for OutboundDelegateMsg {
fn from(req: SubscribeContractRequest) -> Self {
Self::SubscribeContractRequest(req)
}
}
impl From<UnsubscribeContractRequest> for OutboundDelegateMsg {
fn from(req: UnsubscribeContractRequest) -> Self {
Self::UnsubscribeContractRequest(req)
}
}
impl From<DelegateMessage> for OutboundDelegateMsg {
fn from(msg: DelegateMessage) -> Self {
Self::SendDelegateMessage(msg)
}
}
impl OutboundDelegateMsg {
fn deser_user_input_req<'de, D>(deser: D) -> Result<UserInputRequest<'static>, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = <UserInputRequest<'de> as Deserialize>::deserialize(deser)?;
Ok(value.into_owned())
}
pub fn processed(&self) -> bool {
match self {
OutboundDelegateMsg::ApplicationMessage(msg) => msg.processed,
OutboundDelegateMsg::GetContractRequest(msg) => msg.processed,
OutboundDelegateMsg::PutContractRequest(msg) => msg.processed,
OutboundDelegateMsg::UpdateContractRequest(msg) => msg.processed,
OutboundDelegateMsg::SubscribeContractRequest(msg) => msg.processed,
OutboundDelegateMsg::UnsubscribeContractRequest(msg) => msg.processed,
OutboundDelegateMsg::SendDelegateMessage(msg) => msg.processed,
OutboundDelegateMsg::RequestUserInput(_) => true,
OutboundDelegateMsg::ContextUpdated(_) => true,
}
}
pub fn get_context(&self) -> Option<&DelegateContext> {
match self {
OutboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
Some(context)
}
OutboundDelegateMsg::GetContractRequest(GetContractRequest { context, .. }) => {
Some(context)
}
OutboundDelegateMsg::PutContractRequest(PutContractRequest { context, .. }) => {
Some(context)
}
OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
context, ..
}) => Some(context),
OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
context,
..
}) => Some(context),
OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest {
context,
..
}) => Some(context),
OutboundDelegateMsg::SendDelegateMessage(DelegateMessage { context, .. }) => {
Some(context)
}
_ => None,
}
}
pub fn get_mut_context(&mut self) -> Option<&mut DelegateContext> {
match self {
OutboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
Some(context)
}
OutboundDelegateMsg::GetContractRequest(GetContractRequest { context, .. }) => {
Some(context)
}
OutboundDelegateMsg::PutContractRequest(PutContractRequest { context, .. }) => {
Some(context)
}
OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
context, ..
}) => Some(context),
OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
context,
..
}) => Some(context),
OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest {
context,
..
}) => Some(context),
OutboundDelegateMsg::SendDelegateMessage(DelegateMessage { context, .. }) => {
Some(context)
}
_ => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetContractRequest {
pub contract_id: ContractInstanceId,
pub context: DelegateContext,
pub processed: bool,
}
impl GetContractRequest {
pub fn new(contract_id: ContractInstanceId) -> Self {
Self {
contract_id,
context: Default::default(),
processed: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GetContractResponse {
pub contract_id: ContractInstanceId,
pub state: Option<WrappedState>,
pub context: DelegateContext,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PutContractRequest {
pub contract: ContractContainer,
pub state: WrappedState,
#[serde(deserialize_with = "RelatedContracts::deser_related_contracts")]
pub related_contracts: RelatedContracts<'static>,
pub context: DelegateContext,
pub processed: bool,
}
impl PutContractRequest {
pub fn new(
contract: ContractContainer,
state: WrappedState,
related_contracts: RelatedContracts<'static>,
) -> Self {
Self {
contract,
state,
related_contracts,
context: Default::default(),
processed: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PutContractResponse {
pub contract_id: ContractInstanceId,
pub result: Result<(), String>,
pub context: DelegateContext,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UpdateContractRequest {
pub contract_id: ContractInstanceId,
#[serde(deserialize_with = "UpdateContractRequest::deser_update_data")]
pub update: UpdateData<'static>,
pub context: DelegateContext,
pub processed: bool,
}
impl UpdateContractRequest {
pub fn new(contract_id: ContractInstanceId, update: UpdateData<'static>) -> Self {
Self {
contract_id,
update,
context: Default::default(),
processed: false,
}
}
fn deser_update_data<'de, D>(deser: D) -> Result<UpdateData<'static>, D::Error>
where
D: Deserializer<'de>,
{
let value = <UpdateData<'de> as Deserialize>::deserialize(deser)?;
Ok(value.into_owned())
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UpdateContractResponse {
pub contract_id: ContractInstanceId,
pub result: Result<(), String>,
pub context: DelegateContext,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubscribeContractRequest {
pub contract_id: ContractInstanceId,
pub context: DelegateContext,
pub processed: bool,
}
impl SubscribeContractRequest {
pub fn new(contract_id: ContractInstanceId) -> Self {
Self {
contract_id,
context: Default::default(),
processed: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SubscribeContractResponse {
pub contract_id: ContractInstanceId,
pub result: Result<(), String>,
pub context: DelegateContext,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UnsubscribeContractRequest {
pub contract_id: ContractInstanceId,
pub context: DelegateContext,
pub processed: bool,
}
impl UnsubscribeContractRequest {
pub fn new(contract_id: ContractInstanceId) -> Self {
Self {
contract_id,
context: Default::default(),
processed: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UnsubscribeContractResponse {
pub contract_id: ContractInstanceId,
pub result: Result<(), String>,
pub context: DelegateContext,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DelegateMessage {
pub target: DelegateKey,
pub sender: DelegateKey,
pub payload: Vec<u8>,
pub context: DelegateContext,
pub processed: bool,
}
impl DelegateMessage {
pub fn new(target: DelegateKey, sender: DelegateKey, payload: Vec<u8>) -> Self {
Self {
target,
sender,
payload,
context: DelegateContext::default(),
processed: false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ContractNotification {
pub contract_id: ContractInstanceId,
pub new_state: WrappedState,
pub context: DelegateContext,
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct NotificationMessage<'a>(
#[serde_as(as = "serde_with::Bytes")]
#[serde(borrow)]
Cow<'a, [u8]>,
);
impl TryFrom<&serde_json::Value> for NotificationMessage<'static> {
type Error = ();
fn try_from(json: &serde_json::Value) -> Result<NotificationMessage<'static>, ()> {
let bytes = serde_json::to_vec(json).unwrap();
Ok(Self(Cow::Owned(bytes)))
}
}
impl NotificationMessage<'_> {
pub fn into_owned(self) -> NotificationMessage<'static> {
NotificationMessage(self.0.into_owned().into())
}
pub fn bytes(&self) -> &[u8] {
self.0.as_ref()
}
}
#[serde_as]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ClientResponse<'a>(
#[serde_as(as = "serde_with::Bytes")]
#[serde(borrow)]
Cow<'a, [u8]>,
);
impl Deref for ClientResponse<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ClientResponse<'_> {
pub fn new(response: Vec<u8>) -> Self {
Self(response.into())
}
pub fn into_owned(self) -> ClientResponse<'static> {
ClientResponse(self.0.into_owned().into())
}
pub fn bytes(&self) -> &[u8] {
self.0.as_ref()
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct UserInputRequest<'a> {
pub request_id: u32,
#[serde(borrow)]
pub message: NotificationMessage<'a>,
pub responses: Vec<ClientResponse<'a>>,
}
impl UserInputRequest<'_> {
pub fn into_owned(self) -> UserInputRequest<'static> {
UserInputRequest {
request_id: self.request_id,
message: self.message.into_owned(),
responses: self.responses.into_iter().map(|r| r.into_owned()).collect(),
}
}
}
#[doc(hidden)]
pub(crate) mod wasm_interface {
use super::*;
use crate::memory::WasmLinearMem;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct DelegateInterfaceResult {
ptr: i64,
size: u32,
}
impl DelegateInterfaceResult {
pub unsafe fn from_raw(ptr: i64, mem: &WasmLinearMem) -> Self {
let result = Box::leak(Box::from_raw(crate::memory::buf::compute_ptr(
ptr as *mut Self,
mem,
)));
#[cfg(feature = "trace")]
{
tracing::trace!(
"got FFI result @ {ptr} ({:p}) -> {result:?}",
ptr as *mut Self
);
}
*result
}
#[cfg(feature = "contract")]
pub fn into_raw(self) -> i64 {
#[cfg(feature = "trace")]
{
tracing::trace!("returning FFI -> {self:?}");
}
let ptr = Box::into_raw(Box::new(self));
#[cfg(feature = "trace")]
{
tracing::trace!("FFI result ptr: {ptr:p} ({}i64)", ptr as i64);
}
ptr as _
}
pub unsafe fn unwrap(
self,
mem: WasmLinearMem,
) -> Result<Vec<OutboundDelegateMsg>, DelegateError> {
let ptr = crate::memory::buf::compute_ptr(self.ptr as *mut u8, &mem);
let serialized = std::slice::from_raw_parts(ptr as *const u8, self.size as _);
let value: Result<Vec<OutboundDelegateMsg>, DelegateError> =
bincode::deserialize(serialized)
.map_err(|e| DelegateError::Other(format!("{e}")))?;
#[cfg(feature = "trace")]
{
tracing::trace!(
"got result through FFI; addr: {:p} ({}i64, mapped: {ptr:p})
serialized: {serialized:?}
value: {value:?}",
self.ptr as *mut u8,
self.ptr
);
}
value
}
}
impl From<Result<Vec<OutboundDelegateMsg>, DelegateError>> for DelegateInterfaceResult {
fn from(value: Result<Vec<OutboundDelegateMsg>, DelegateError>) -> Self {
let serialized = bincode::serialize(&value).unwrap();
let size = serialized.len() as _;
let ptr = serialized.as_ptr();
#[cfg(feature = "trace")]
{
tracing::trace!(
"sending result through FFI; addr: {ptr:p} ({}),\n serialized: {serialized:?}\n value: {value:?}",
ptr as i64
);
}
std::mem::forget(serialized);
Self {
ptr: ptr as i64,
size,
}
}
}
}
#[cfg(test)]
mod message_origin_tests {
use super::*;
#[test]
fn webapp_origin_wire_format_is_stable() {
let id = ContractInstanceId::new([0xABu8; 32]);
let origin = MessageOrigin::WebApp(id);
let encoded = bincode::serialize(&origin).unwrap();
let mut expected = vec![0u8, 0, 0, 0];
expected.extend_from_slice(&[0xABu8; 32]);
assert_eq!(encoded, expected);
}
#[test]
fn delegate_origin_wire_format_is_stable() {
let key = DelegateKey::new([0x11u8; 32], crate::code_hash::CodeHash::new([0x22u8; 32]));
let origin = MessageOrigin::Delegate(key);
let encoded = bincode::serialize(&origin).unwrap();
let mut expected = vec![1u8, 0, 0, 0];
expected.extend_from_slice(&[0x11u8; 32]);
expected.extend_from_slice(&[0x22u8; 32]);
assert_eq!(encoded, expected);
let decoded: MessageOrigin = bincode::deserialize(&encoded).unwrap();
assert!(matches!(decoded, MessageOrigin::Delegate(_)));
}
#[test]
fn inbound_delegate_msg_wire_format_is_stable() {
let msg = InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC]));
let encoded = bincode::serialize(&msg).unwrap();
assert_eq!(
encoded[..4],
[0, 0, 0, 0],
"ApplicationMessage must stay at variant tag 0 on the wire; \
reordering InboundDelegateMsg variants is a wire-format break"
);
let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&encoded).unwrap();
assert!(matches!(decoded, InboundDelegateMsg::ApplicationMessage(_)));
}
#[test]
fn inbound_wakeup_fired_wire_format_is_stable() {
let msg = InboundDelegateMsg::WakeupFired {
tag: vec![0xAA, 0xBB],
};
let encoded = bincode::serialize(&msg).unwrap();
let mut expected = vec![9u8, 0, 0, 0];
expected.extend_from_slice(&[2, 0, 0, 0, 0, 0, 0, 0]);
expected.extend_from_slice(&[0xAA, 0xBB]);
assert_eq!(
encoded, expected,
"WakeupFired must stay at variant tag 9 with a stable payload layout"
);
let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&encoded).unwrap();
assert!(matches!(
decoded,
InboundDelegateMsg::WakeupFired { tag } if tag == vec![0xAA, 0xBB]
));
}
}
#[cfg(test)]
mod delegate_wire_compat {
use super::*;
use crate::contract_interface::WrappedContract;
use crate::prelude::ContractCode;
use crate::versioning::ContractWasmAPIVersion;
use std::sync::Arc;
const INBOUND_VARIANT_COUNT: u32 = 10;
const OUTBOUND_VARIANT_COUNT: u32 = 9;
fn instance_id() -> ContractInstanceId {
ContractInstanceId::new([0x5Au8; 32])
}
fn delegate_key() -> DelegateKey {
DelegateKey::new([0x11u8; 32], CodeHash::new([0x22u8; 32]))
}
fn contract_container() -> ContractContainer {
ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new(
Arc::new(ContractCode::from(vec![1u8, 2, 3])),
Parameters::from(vec![9u8, 8, 7]),
)))
}
fn wire_tag(encoded: &[u8]) -> u32 {
u32::from_le_bytes(
encoded[..4]
.try_into()
.expect("a bincode enum encoding starts with a 4-byte tag"),
)
}
fn pinned_inbound_tag(msg: &InboundDelegateMsg<'_>) -> u32 {
match msg {
InboundDelegateMsg::ApplicationMessage(_) => 0,
InboundDelegateMsg::UserResponse(_) => 1,
InboundDelegateMsg::GetContractResponse(_) => 2,
InboundDelegateMsg::PutContractResponse(_) => 3,
InboundDelegateMsg::UpdateContractResponse(_) => 4,
InboundDelegateMsg::SubscribeContractResponse(_) => 5,
InboundDelegateMsg::ContractNotification(_) => 6,
InboundDelegateMsg::DelegateMessage(_) => 7,
InboundDelegateMsg::UnsubscribeContractResponse(_) => 8,
InboundDelegateMsg::WakeupFired { .. } => 9,
}
}
fn pinned_outbound_tag(msg: &OutboundDelegateMsg) -> u32 {
match msg {
OutboundDelegateMsg::ApplicationMessage(_) => 0,
OutboundDelegateMsg::RequestUserInput(_) => 1,
OutboundDelegateMsg::ContextUpdated(_) => 2,
OutboundDelegateMsg::GetContractRequest(_) => 3,
OutboundDelegateMsg::PutContractRequest(_) => 4,
OutboundDelegateMsg::UpdateContractRequest(_) => 5,
OutboundDelegateMsg::SubscribeContractRequest(_) => 6,
OutboundDelegateMsg::SendDelegateMessage(_) => 7,
OutboundDelegateMsg::UnsubscribeContractRequest(_) => 8,
}
}
fn every_inbound() -> Vec<InboundDelegateMsg<'static>> {
let id = instance_id();
let ctx = DelegateContext::default();
vec![
InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC])),
InboundDelegateMsg::UserResponse(UserInputResponse {
request_id: 7,
response: ClientResponse::new(vec![0x01]),
context: ctx.clone(),
}),
InboundDelegateMsg::GetContractResponse(GetContractResponse {
contract_id: id,
state: None,
context: ctx.clone(),
}),
InboundDelegateMsg::PutContractResponse(PutContractResponse {
contract_id: id,
result: Ok(()),
context: ctx.clone(),
}),
InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
contract_id: id,
result: Ok(()),
context: ctx.clone(),
}),
InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
contract_id: id,
result: Ok(()),
context: ctx.clone(),
}),
InboundDelegateMsg::ContractNotification(ContractNotification {
contract_id: id,
new_state: WrappedState::new(vec![0xAB]),
context: ctx.clone(),
}),
InboundDelegateMsg::DelegateMessage(DelegateMessage::new(
delegate_key(),
delegate_key(),
vec![0xEE],
)),
InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
contract_id: id,
result: Ok(()),
context: ctx.clone(),
}),
InboundDelegateMsg::WakeupFired {
tag: vec![0xAA, 0xBB],
},
]
}
fn every_outbound() -> Vec<OutboundDelegateMsg> {
let id = instance_id();
vec![
OutboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC])),
OutboundDelegateMsg::RequestUserInput(UserInputRequest {
request_id: 7,
message: NotificationMessage(Cow::Owned(vec![0x02])),
responses: vec![],
}),
OutboundDelegateMsg::ContextUpdated(DelegateContext::default()),
OutboundDelegateMsg::GetContractRequest(GetContractRequest::new(id)),
OutboundDelegateMsg::PutContractRequest(PutContractRequest::new(
contract_container(),
WrappedState::new(vec![0xAB]),
RelatedContracts::default(),
)),
OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest::new(
id,
UpdateData::State(vec![0xAB].into()),
)),
OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest::new(id)),
OutboundDelegateMsg::SendDelegateMessage(DelegateMessage::new(
delegate_key(),
delegate_key(),
vec![0xEE],
)),
OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest::new(id)),
]
}
#[test]
fn delegate_msg_variant_tags_are_pinned() {
for msg in every_inbound() {
let expected = pinned_inbound_tag(&msg);
let encoded = bincode::serialize(&msg).expect("inbound must serialize");
assert_eq!(
wire_tag(&encoded),
expected,
"InboundDelegateMsg::{msg:?} moved off wire tag {expected}; inserting, \
reordering or removing variants breaks deployed delegate WASM"
);
}
for msg in every_outbound() {
let expected = pinned_outbound_tag(&msg);
let encoded = bincode::serialize(&msg).expect("outbound must serialize");
assert_eq!(
wire_tag(&encoded),
expected,
"OutboundDelegateMsg::{msg:?} moved off wire tag {expected}; inserting, \
reordering or removing variants breaks deployed delegate WASM"
);
}
}
#[test]
fn every_variant_is_covered_by_the_pin() {
let mut inbound: Vec<u32> = every_inbound().iter().map(pinned_inbound_tag).collect();
inbound.sort_unstable();
assert_eq!(
inbound,
(0..INBOUND_VARIANT_COUNT).collect::<Vec<_>>(),
"every_inbound must contain each InboundDelegateMsg variant exactly once"
);
let mut outbound: Vec<u32> = every_outbound().iter().map(pinned_outbound_tag).collect();
outbound.sort_unstable();
assert_eq!(
outbound,
(0..OUTBOUND_VARIANT_COUNT).collect::<Vec<_>>(),
"every_outbound must contain each OutboundDelegateMsg variant exactly once"
);
}
#[test]
fn an_unpinned_variant_fails_this_test() {
fn assert_rejected_as_unknown_variant(err: &bincode::Error, tag: u32, which: &str) {
let msg = err.to_string();
assert!(
msg.contains("variant index"),
"tag {tag} on {which} failed for the wrong reason ({msg}); the tag itself must \
still be unknown, otherwise a variant was added without updating the count, \
the pinned_*_tag match and the every_* list"
);
}
let mut probe = INBOUND_VARIANT_COUNT.to_le_bytes().to_vec();
probe.extend_from_slice(&[0u8; 256]);
let err = match bincode::deserialize::<InboundDelegateMsg<'_>>(&probe) {
Ok(v) => panic!(
"tag {INBOUND_VARIANT_COUNT} must not decode as an InboundDelegateMsg, got {v:?}"
),
Err(e) => e,
};
assert_rejected_as_unknown_variant(&err, INBOUND_VARIANT_COUNT, "InboundDelegateMsg");
let mut probe = OUTBOUND_VARIANT_COUNT.to_le_bytes().to_vec();
probe.extend_from_slice(&[0u8; 256]);
let err = match bincode::deserialize::<OutboundDelegateMsg>(&probe) {
Ok(v) => panic!(
"tag {OUTBOUND_VARIANT_COUNT} must not decode as an OutboundDelegateMsg, got {v:?}"
),
Err(e) => e,
};
assert_rejected_as_unknown_variant(&err, OUTBOUND_VARIANT_COUNT, "OutboundDelegateMsg");
let mut control = (INBOUND_VARIANT_COUNT - 1).to_le_bytes().to_vec();
control.extend_from_slice(&[0u8; 256]);
bincode::deserialize::<InboundDelegateMsg<'_>>(&control).expect(
"the LAST inbound variant's payload must be decodable from zeros, or this probe can \
no longer tell an unknown tag from an unparseable payload. If a variant whose \
payload rejects zeros was just appended, do not delete this — point the control at \
a variant that still decodes from zeros",
);
let mut control = (OUTBOUND_VARIANT_COUNT - 1).to_le_bytes().to_vec();
control.extend_from_slice(&[0u8; 256]);
bincode::deserialize::<OutboundDelegateMsg>(&control).expect(
"the LAST outbound variant's payload must be decodable from zeros — see the inbound \
control above for what to do if that stops being true",
);
}
#[test]
fn a_hand_built_old_encoder_payload_decodes_into_the_same_variant() {
let mut old_payload = vec![6u8, 0, 0, 0];
old_payload.extend_from_slice(&[0x5Au8; 32]);
old_payload.extend_from_slice(&0u64.to_le_bytes()); old_payload.extend_from_slice(&0u64.to_le_bytes());
let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&old_payload)
.expect("a payload predating any appended variant must still decode");
match decoded {
InboundDelegateMsg::ContractNotification(n) => {
assert_eq!(n.contract_id, instance_id());
}
other => panic!("an old ContractNotification decoded as {other:?}"),
}
}
#[test]
fn a_new_variant_does_not_decode_on_an_old_receiver() {
#[allow(dead_code)]
#[derive(serde::Deserialize, Debug)]
enum OldOutboundTagSpace {
V0,
V1,
V2,
V3,
V4,
V5,
V6,
}
let new_msg = bincode::serialize(&OutboundDelegateMsg::SendDelegateMessage(
DelegateMessage::new(delegate_key(), delegate_key(), vec![0xEE]),
))
.expect("outbound must serialize");
assert_eq!(wire_tag(&new_msg), 7);
let decoded = bincode::deserialize::<OldOutboundTagSpace>(&new_msg);
assert!(
decoded.is_err(),
"a receiver that predates a variant must REJECT it, not mis-decode it; \
if this ever passes, the compatibility rule documented on \
OutboundDelegateMsg is wrong and delegates are silently misreading messages"
);
}
#[test]
fn the_unsubscribe_pair_round_trips_and_disturbs_nothing_older() {
let id = instance_id();
let req =
OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest::new(id));
let encoded = bincode::serialize(&req).expect("request must serialize");
assert_eq!(wire_tag(&encoded), 8, "unsubscribe request is frozen at 8");
match bincode::deserialize::<OutboundDelegateMsg>(&encoded).expect("must round-trip") {
OutboundDelegateMsg::UnsubscribeContractRequest(r) => {
assert_eq!(r.contract_id, id);
assert!(!r.processed);
}
other => panic!("round-tripped into {other:?}"),
}
let resp = InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
contract_id: id,
result: Ok(()),
context: DelegateContext::default(),
});
let encoded = bincode::serialize(&resp).expect("response must serialize");
assert_eq!(wire_tag(&encoded), 8, "unsubscribe response is frozen at 8");
match bincode::deserialize::<InboundDelegateMsg<'_>>(&encoded).expect("must round-trip") {
InboundDelegateMsg::UnsubscribeContractResponse(r) => {
assert_eq!(r.contract_id, id);
assert!(r.result.is_ok());
}
other => panic!("round-tripped into {other:?}"),
}
let mut expected_resp = vec![8u8, 0, 0, 0];
expected_resp.extend_from_slice(&[0x5Au8; 32]); expected_resp.extend_from_slice(&0u32.to_le_bytes()); expected_resp.extend_from_slice(&0u64.to_le_bytes()); assert_eq!(
encoded, expected_resp,
"UnsubscribeContractResponse layout is frozen: tag, contract_id, result, context"
);
let expected_req = {
let mut v = vec![8u8, 0, 0, 0];
v.extend_from_slice(&[0x5Au8; 32]); v.extend_from_slice(&0u64.to_le_bytes()); v.push(0u8); v
};
assert_eq!(
bincode::serialize(&req).expect("request must serialize"),
expected_req,
"UnsubscribeContractRequest layout is frozen: tag, contract_id, context, processed"
);
let err_resp =
InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
contract_id: id,
result: Err("nope".to_string()),
context: DelegateContext::default(),
});
match bincode::deserialize::<InboundDelegateMsg<'_>>(
&bincode::serialize(&err_resp).expect("must serialize"),
)
.expect("must round-trip")
{
InboundDelegateMsg::UnsubscribeContractResponse(r) => {
assert_eq!(r.result.unwrap_err(), "nope");
}
other => panic!("error response round-tripped into {other:?}"),
}
let mut pre_0_9_0 = vec![6u8, 0, 0, 0];
pre_0_9_0.extend_from_slice(&[0x5Au8; 32]);
pre_0_9_0.extend_from_slice(&0u64.to_le_bytes());
pre_0_9_0.extend_from_slice(&0u64.to_le_bytes());
match bincode::deserialize::<InboundDelegateMsg<'_>>(&pre_0_9_0)
.expect("a pre-0.10.0 payload must still decode")
{
InboundDelegateMsg::ContractNotification(n) => assert_eq!(n.contract_id, id),
other => panic!("a pre-0.10.0 ContractNotification decoded as {other:?}"),
}
}
#[test]
fn every_inbound_variant_with_a_context_exposes_it() {
for mut msg in every_inbound() {
let tag = pinned_inbound_tag(&msg);
if matches!(msg, InboundDelegateMsg::WakeupFired { .. }) {
assert!(
msg.get_context().is_none() && msg.get_mut_context().is_none(),
"WakeupFired is documented as carrying no context; if it grew one, remove this exemption rather than widening it"
);
continue;
}
assert!(
msg.get_context().is_some(),
"InboundDelegateMsg tag {tag} has a context field but get_context returned None; \
the `_ => None` wildcard hides a missing arm"
);
assert!(
msg.get_mut_context().is_some(),
"InboundDelegateMsg tag {tag} has a context field but get_mut_context returned \
None; the two accessors must agree"
);
}
}
#[test]
fn every_outbound_variant_with_a_context_exposes_it() {
for mut msg in every_outbound() {
let tag = pinned_outbound_tag(&msg);
let has_no_context = matches!(
msg,
OutboundDelegateMsg::RequestUserInput(_) | OutboundDelegateMsg::ContextUpdated(_)
);
if has_no_context {
continue;
}
assert!(
msg.get_context().is_some(),
"OutboundDelegateMsg tag {tag} has a context field but get_context returned None"
);
assert!(
msg.get_mut_context().is_some(),
"OutboundDelegateMsg tag {tag} has a context field but get_mut_context returned \
None; the two accessors must agree"
);
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
enum NewMsgWithPayload {
First(u32),
Second(bool),
Appended(String),
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
enum OldMsgWithCatchAll {
First(u32),
#[serde(other)]
Unknown,
}
#[derive(Serialize, Deserialize, Debug, PartialEq)]
enum NewMsgUnitAppended {
First(u32),
Second(bool),
AppendedUnit,
}
#[test]
fn the_attribute_is_what_bridges_the_gap() {
fn tag_of(bytes: &[u8]) -> u32 {
u32::from_le_bytes(
bytes[..4]
.try_into()
.expect("a bincode enum tag is 4 bytes"),
)
}
let appended =
tag_of(&bincode::serialize(&NewMsgWithPayload::Appended("x".into())).unwrap());
let absorbed_from = (0u32..16)
.find(|t| {
let mut probe = t.to_le_bytes().to_vec();
probe.extend_from_slice(&[0u8; 32]);
matches!(
bincode::deserialize::<OldMsgWithCatchAll>(&probe),
Ok(OldMsgWithCatchAll::Unknown)
)
})
.expect("OldMsgWithCatchAll must absorb some tag; it has #[serde(other)]");
assert!(
appended > absorbed_from,
"`Appended` is at tag {appended} and OldMsgWithCatchAll absorbs from tag \
{absorbed_from}: the mocks have re-aligned, so the serde(other) tests below \
are vacuous and pass with the attribute deleted. Move `Appended` above the \
catch-all index again rather than adjusting this test."
);
}
#[test]
fn serde_other_does_absorb_an_unknown_tag_in_bincode() {
let encoded = bincode::serialize(&NewMsgWithPayload::Appended("x".into())).unwrap();
let decoded: OldMsgWithCatchAll =
bincode::deserialize(&encoded).expect("serde(other) absorbs the unknown tag");
assert_eq!(decoded, OldMsgWithCatchAll::Unknown);
}
#[test]
fn the_catch_all_silently_corrupts_trailing_data() {
let encoded =
bincode::serialize(&(NewMsgWithPayload::Appended("hello-future".into()), 4242u32))
.unwrap();
let (variant, trailing): (OldMsgWithCatchAll, u32) =
bincode::deserialize(&encoded).expect("decodes, which is the problem");
assert_eq!(variant, OldMsgWithCatchAll::Unknown);
assert_ne!(
trailing, 4242,
"if this ever equals 4242, serde(other) stopped eating the payload \
and this section of WIRE-FORMAT.md needs revisiting"
);
}
#[test]
fn the_catch_all_is_clean_for_a_unit_variant() {
let encoded = bincode::serialize(&(NewMsgUnitAppended::AppendedUnit, 4242u32)).unwrap();
let (variant, trailing): (OldMsgWithCatchAll, u32) =
bincode::deserialize(&encoded).expect("unit variant leaves nothing behind");
assert_eq!(variant, OldMsgWithCatchAll::Unknown);
assert_eq!(
trailing, 4242,
"a unit unknown variant must NOT corrupt what follows — this is the \
case that misleads, and it is why the rule is unconditional"
);
}
}