freenet_stdlib/delegate_interface.rs
1use std::{
2 borrow::{Borrow, Cow},
3 fmt::Display,
4 fs::File,
5 io::Read,
6 ops::Deref,
7 path::Path,
8};
9
10use blake3::{traits::digest::Digest, Hasher as Blake3};
11use serde::{Deserialize, Deserializer, Serialize};
12use serde_with::serde_as;
13
14use crate::generated::client_request::{
15 DelegateKey as FbsDelegateKey, InboundDelegateMsg as FbsInboundDelegateMsg,
16 InboundDelegateMsgType,
17};
18
19use crate::common_generated::common::SecretsId as FbsSecretsId;
20
21use crate::client_api::{fixed_size_field, unknown_union_discriminant, TryFromFbs, WsApiError};
22use crate::contract_interface::{RelatedContracts, UpdateData, CONTRACT_KEY_SIZE};
23use crate::prelude::{ContractInstanceId, WrappedState};
24use crate::versioning::ContractContainer;
25use crate::{code_hash::CodeHash, prelude::Parameters};
26
27const DELEGATE_HASH_LENGTH: usize = 32;
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub struct Delegate<'a> {
31 #[serde(borrow)]
32 parameters: Parameters<'a>,
33 #[serde(borrow)]
34 pub data: DelegateCode<'a>,
35 key: DelegateKey,
36}
37
38impl Delegate<'_> {
39 pub fn key(&self) -> &DelegateKey {
40 &self.key
41 }
42
43 pub fn code(&self) -> &DelegateCode<'_> {
44 &self.data
45 }
46
47 pub fn code_hash(&self) -> &CodeHash {
48 &self.data.code_hash
49 }
50
51 pub fn params(&self) -> &Parameters<'_> {
52 &self.parameters
53 }
54
55 pub fn into_owned(self) -> Delegate<'static> {
56 Delegate {
57 parameters: self.parameters.into_owned(),
58 data: self.data.into_owned(),
59 key: self.key,
60 }
61 }
62
63 pub fn size(&self) -> usize {
64 self.parameters.size() + self.data.size()
65 }
66
67 pub(crate) fn deserialize_delegate<'de, D>(deser: D) -> Result<Delegate<'static>, D::Error>
68 where
69 D: Deserializer<'de>,
70 {
71 let data: Delegate<'de> = Deserialize::deserialize(deser)?;
72 Ok(data.into_owned())
73 }
74}
75
76impl PartialEq for Delegate<'_> {
77 fn eq(&self, other: &Self) -> bool {
78 self.key == other.key
79 }
80}
81
82impl Eq for Delegate<'_> {}
83
84impl<'a> From<(&DelegateCode<'a>, &Parameters<'a>)> for Delegate<'a> {
85 fn from((data, parameters): (&DelegateCode<'a>, &Parameters<'a>)) -> Self {
86 Self {
87 key: DelegateKey::from_params_and_code(parameters, data),
88 parameters: parameters.clone(),
89 data: data.clone(),
90 }
91 }
92}
93
94/// Executable delegate
95#[derive(Debug, Serialize, Deserialize, Clone)]
96#[serde_as]
97pub struct DelegateCode<'a> {
98 #[serde_as(as = "serde_with::Bytes")]
99 #[serde(borrow)]
100 pub(crate) data: Cow<'a, [u8]>,
101 // todo: skip serializing and instead compute it
102 pub(crate) code_hash: CodeHash,
103}
104
105impl DelegateCode<'static> {
106 /// Loads the contract raw wasm module, without any version.
107 pub fn load_raw(path: &Path) -> Result<Self, std::io::Error> {
108 let contract_data = Self::load_bytes(path)?;
109 Ok(DelegateCode::from(contract_data))
110 }
111
112 pub(crate) fn load_bytes(path: &Path) -> Result<Vec<u8>, std::io::Error> {
113 let mut contract_file = File::open(path)?;
114 let mut contract_data = if let Ok(md) = contract_file.metadata() {
115 Vec::with_capacity(md.len() as usize)
116 } else {
117 Vec::new()
118 };
119 contract_file.read_to_end(&mut contract_data)?;
120 Ok(contract_data)
121 }
122}
123
124impl DelegateCode<'_> {
125 /// Delegate code hash.
126 pub fn hash(&self) -> &CodeHash {
127 &self.code_hash
128 }
129
130 /// Returns the `Base58` string representation of the delegate key.
131 pub fn hash_str(&self) -> String {
132 Self::encode_hash(&self.code_hash.0)
133 }
134
135 /// Reference to delegate code.
136 pub fn data(&self) -> &[u8] {
137 &self.data
138 }
139
140 /// Returns the `Base58` string representation of a hash.
141 pub fn encode_hash(hash: &[u8; DELEGATE_HASH_LENGTH]) -> String {
142 bs58::encode(hash)
143 .with_alphabet(bs58::Alphabet::BITCOIN)
144 .into_string()
145 }
146
147 pub fn into_owned(self) -> DelegateCode<'static> {
148 DelegateCode {
149 code_hash: self.code_hash,
150 data: Cow::from(self.data.into_owned()),
151 }
152 }
153
154 pub fn size(&self) -> usize {
155 self.data.len()
156 }
157}
158
159impl PartialEq for DelegateCode<'_> {
160 fn eq(&self, other: &Self) -> bool {
161 self.code_hash == other.code_hash
162 }
163}
164
165impl Eq for DelegateCode<'_> {}
166
167impl AsRef<[u8]> for DelegateCode<'_> {
168 fn as_ref(&self) -> &[u8] {
169 self.data.borrow()
170 }
171}
172
173impl From<Vec<u8>> for DelegateCode<'static> {
174 fn from(data: Vec<u8>) -> Self {
175 let key = CodeHash::from_code(data.as_slice());
176 DelegateCode {
177 data: Cow::from(data),
178 code_hash: key,
179 }
180 }
181}
182
183impl<'a> From<&'a [u8]> for DelegateCode<'a> {
184 fn from(code: &'a [u8]) -> Self {
185 let key = CodeHash::from_code(code);
186 DelegateCode {
187 data: Cow::from(code),
188 code_hash: key,
189 }
190 }
191}
192
193#[serde_as]
194#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
195pub struct DelegateKey {
196 #[serde_as(as = "[_; DELEGATE_HASH_LENGTH]")]
197 key: [u8; DELEGATE_HASH_LENGTH],
198 code_hash: CodeHash,
199}
200
201impl From<DelegateKey> for SecretsId {
202 fn from(key: DelegateKey) -> SecretsId {
203 SecretsId {
204 hash: key.key,
205 key: vec![],
206 }
207 }
208}
209
210impl DelegateKey {
211 pub const fn new(key: [u8; DELEGATE_HASH_LENGTH], code_hash: CodeHash) -> Self {
212 Self { key, code_hash }
213 }
214
215 fn from_params_and_code<'a>(
216 params: impl Borrow<Parameters<'a>>,
217 wasm_code: impl Borrow<DelegateCode<'a>>,
218 ) -> Self {
219 let code = wasm_code.borrow();
220 let key = generate_id(params.borrow(), code);
221 Self {
222 key,
223 code_hash: *code.hash(),
224 }
225 }
226
227 pub fn encode(&self) -> String {
228 bs58::encode(self.key)
229 .with_alphabet(bs58::Alphabet::BITCOIN)
230 .into_string()
231 }
232
233 pub fn code_hash(&self) -> &CodeHash {
234 &self.code_hash
235 }
236
237 pub fn bytes(&self) -> &[u8] {
238 self.key.as_ref()
239 }
240
241 pub fn from_params(
242 code_hash: impl Into<String>,
243 parameters: &Parameters,
244 ) -> Result<Self, bs58::decode::Error> {
245 let mut code_key = [0; DELEGATE_HASH_LENGTH];
246 bs58::decode(code_hash.into())
247 .with_alphabet(bs58::Alphabet::BITCOIN)
248 .onto(&mut code_key)?;
249 let mut hasher = Blake3::new();
250 hasher.update(code_key.as_slice());
251 hasher.update(parameters.as_ref());
252 let full_key_arr = hasher.finalize();
253
254 debug_assert_eq!(full_key_arr[..].len(), DELEGATE_HASH_LENGTH);
255 let mut key = [0; DELEGATE_HASH_LENGTH];
256 key.copy_from_slice(&full_key_arr);
257
258 Ok(Self {
259 key,
260 code_hash: CodeHash(code_key),
261 })
262 }
263}
264
265impl Deref for DelegateKey {
266 type Target = [u8; DELEGATE_HASH_LENGTH];
267
268 fn deref(&self) -> &Self::Target {
269 &self.key
270 }
271}
272
273impl Display for DelegateKey {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 write!(f, "{}", self.encode())
276 }
277}
278
279impl<'a> TryFromFbs<&FbsDelegateKey<'a>> for DelegateKey {
280 fn try_decode_fbs(key: &FbsDelegateKey<'a>) -> Result<Self, WsApiError> {
281 // Both fields are `(required)` in the schema and BOTH need an explicit
282 // length check, because the verifier only guarantees presence. `key`
283 // used to be a bare `copy_from_slice` into a `[0; 32]`, which panics on
284 // a length mismatch, while `code_hash` one line below was already
285 // length-checked inside `CodeHash::try_from`. Keep them symmetric: a
286 // future field added here needs the same treatment.
287 let key_bytes =
288 fixed_size_field::<DELEGATE_HASH_LENGTH>("DelegateKey.key", key.key().bytes())?;
289 // `CodeHash::try_from` DOES length-check, so this field never panicked —
290 // but its error stringifies to "invalid data", naming neither the field
291 // nor the length. Symmetric treatment means the same message shape, not
292 // merely the same safety, so it goes through the same helper.
293 let code_hash = CodeHash::new(fixed_size_field::<CONTRACT_KEY_SIZE>(
294 "DelegateKey.code_hash",
295 key.code_hash().bytes(),
296 )?);
297 Ok(DelegateKey {
298 key: key_bytes,
299 code_hash,
300 })
301 }
302}
303
304/// Type of errors during interaction with a delegate.
305///
306/// Marked `#[non_exhaustive]` so future error variants can be added without a
307/// source-level break. Downstream `match` sites must include a wildcard arm.
308#[non_exhaustive]
309#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
310pub enum DelegateError {
311 #[error("de/serialization error: {0}")]
312 Deser(String),
313 #[error("{0}")]
314 Other(String),
315}
316
317fn generate_id<'a>(
318 parameters: &Parameters<'a>,
319 code_data: &DelegateCode<'a>,
320) -> [u8; DELEGATE_HASH_LENGTH] {
321 let contract_hash = code_data.hash();
322
323 let mut hasher = Blake3::new();
324 hasher.update(contract_hash.0.as_slice());
325 hasher.update(parameters.as_ref());
326 let full_key_arr = hasher.finalize();
327
328 debug_assert_eq!(full_key_arr[..].len(), DELEGATE_HASH_LENGTH);
329 let mut key = [0; DELEGATE_HASH_LENGTH];
330 key.copy_from_slice(&full_key_arr);
331 key
332}
333
334#[serde_as]
335#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
336pub struct SecretsId {
337 #[serde_as(as = "serde_with::Bytes")]
338 key: Vec<u8>,
339 #[serde_as(as = "[_; 32]")]
340 hash: [u8; 32],
341}
342
343impl SecretsId {
344 pub fn new(key: Vec<u8>) -> Self {
345 let mut hasher = Blake3::new();
346 hasher.update(&key);
347 let hashed = hasher.finalize();
348 let mut hash = [0; 32];
349 hash.copy_from_slice(&hashed);
350 Self { key, hash }
351 }
352
353 pub fn encode(&self) -> String {
354 bs58::encode(self.hash)
355 .with_alphabet(bs58::Alphabet::BITCOIN)
356 .into_string()
357 }
358
359 pub fn hash(&self) -> &[u8; 32] {
360 &self.hash
361 }
362 pub fn key(&self) -> &[u8] {
363 self.key.as_slice()
364 }
365}
366
367impl Display for SecretsId {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 write!(f, "{}", self.encode())
370 }
371}
372
373impl<'a> TryFromFbs<&FbsSecretsId<'a>> for SecretsId {
374 fn try_decode_fbs(key: &FbsSecretsId<'a>) -> Result<Self, WsApiError> {
375 // No production caller reaches this decoder today — `common.SecretsId`
376 // appears in no client-request table. It is fixed anyway because the
377 // `copy_from_slice` it replaces is a loaded gun for whoever wires it up:
378 // `hash` is `(required)`, which the verifier reads as "present", not
379 // "32 bytes", so the first client to send a short one would have
380 // panicked the connection task.
381 let key_hash = fixed_size_field::<32>("SecretsId.hash", key.hash().bytes())?;
382 Ok(SecretsId {
383 key: key.key().bytes().to_vec(),
384 hash: key_hash,
385 })
386 }
387}
388
389/// Identifies where an inbound application message originated from.
390///
391/// When a web app sends a message to a delegate through the WebSocket API with
392/// an authentication token, the runtime resolves the token to the originating
393/// contract and wraps it in `MessageOrigin::WebApp`. When one delegate sends a
394/// message to another via [`OutboundDelegateMsg::SendDelegateMessage`], the
395/// runtime attests the caller's identity in `MessageOrigin::Delegate`.
396/// Delegates receive this as the `origin` parameter of
397/// [`DelegateInterface::process`].
398///
399/// This enum is `#[non_exhaustive]`: downstream code matching on it must
400/// include a wildcard arm so future variants can be added without a
401/// source-level breaking change.
402#[non_exhaustive]
403#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
404pub enum MessageOrigin {
405 /// The message was sent by a web application backed by the given contract.
406 WebApp(ContractInstanceId),
407 /// The message was sent by another delegate via
408 /// [`OutboundDelegateMsg::SendDelegateMessage`]. The carried key is the
409 /// runtime-attested identity of the calling delegate; the receiver can
410 /// trust it to make authorization decisions.
411 ///
412 /// Note: an inter-delegate message **replaces** rather than composes with
413 /// any inherited `WebApp` origin the calling delegate may itself hold.
414 /// The receiver sees only `Delegate(caller_key)` for the duration of the
415 /// call, and does not gain contract access on behalf of any web app the
416 /// caller was acting for. Authorization should be made on the calling
417 /// delegate's identity alone.
418 Delegate(DelegateKey),
419}
420
421/// A Delegate is a webassembly code designed to act as an agent for the user on
422/// Freenet. Delegates can:
423///
424/// * Store private data on behalf of the user
425/// * Create, read, and modify contracts
426/// * Create other delegates
427/// * Send and receive messages from other delegates and user interfaces
428/// * Ask the user questions and receive answers
429///
430/// Example use cases:
431///
432/// * A delegate stores a private key for the user, other components can ask
433/// the delegate to sign messages, it will ask the user for permission
434/// * A delegate monitors an inbox contract and downloads new messages when
435/// they arrive
436///
437/// # Example
438///
439/// ```ignore
440/// use freenet_stdlib::prelude::*;
441///
442/// struct MyDelegate;
443///
444/// #[delegate]
445/// impl DelegateInterface for MyDelegate {
446/// fn process(
447/// ctx: &mut DelegateCtx,
448/// _params: Parameters<'static>,
449/// _origin: Option<MessageOrigin>,
450/// message: InboundDelegateMsg,
451/// ) -> Result<Vec<OutboundDelegateMsg>, DelegateError> {
452/// // Access secrets synchronously - no round-trip needed!
453/// if let Some(key) = ctx.get_secret(b"private_key") {
454/// // use key...
455/// }
456/// ctx.set_secret(b"new_key", b"value");
457///
458/// // Read/write context for temporary state within a batch
459/// ctx.write(b"some state");
460///
461/// Ok(vec![])
462/// }
463/// }
464/// ```
465pub trait DelegateInterface {
466 /// Process inbound message, producing zero or more outbound messages in response.
467 ///
468 /// # Arguments
469 /// - `ctx`: Mutable handle to the delegate's execution environment. Provides:
470 /// - **Context** (temporary): `read()`, `write()`, `len()`, `clear()` - state within a batch
471 /// - **Secrets** (persistent): `get_secret()`, `set_secret()`, `has_secret()`, `remove_secret()`
472 /// - `parameters`: The delegate's initialization parameters.
473 /// - `origin`: An optional [`MessageOrigin`] identifying where the message came from.
474 /// For messages sent by web applications, this is `MessageOrigin::WebApp(contract_id)`.
475 /// - `message`: The inbound message to process.
476 fn process(
477 ctx: &mut crate::delegate_host::DelegateCtx,
478 parameters: Parameters<'static>,
479 origin: Option<MessageOrigin>,
480 message: InboundDelegateMsg,
481 ) -> Result<Vec<OutboundDelegateMsg>, DelegateError>;
482}
483
484#[serde_as]
485#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
486pub struct DelegateContext(#[serde_as(as = "serde_with::Bytes")] Vec<u8>);
487
488impl DelegateContext {
489 pub const MAX_SIZE: usize = 4096 * 10 * 10;
490
491 pub fn new(bytes: Vec<u8>) -> Self {
492 assert!(bytes.len() < Self::MAX_SIZE);
493 Self(bytes)
494 }
495
496 pub fn append(&mut self, bytes: &mut Vec<u8>) {
497 assert!(self.0.len() + bytes.len() < Self::MAX_SIZE);
498 self.0.append(bytes)
499 }
500
501 pub fn replace(&mut self, bytes: Vec<u8>) {
502 assert!(bytes.len() < Self::MAX_SIZE);
503 let _ = std::mem::replace(&mut self.0, bytes);
504 }
505}
506
507impl AsRef<[u8]> for DelegateContext {
508 fn as_ref(&self) -> &[u8] {
509 &self.0
510 }
511}
512
513/// Messages delivered **into** a delegate's `process()` function.
514///
515/// This is the inbound counterpart of [`OutboundDelegateMsg`] and sits on the
516/// host↔delegate wire boundary.
517///
518/// Marked `#[non_exhaustive]` so future variants can be added without a
519/// source-level break; downstream `match` sites must include a wildcard arm.
520/// [`OutboundDelegateMsg`] is deliberately **not** marked, and the asymmetry is
521/// the point — see the rationale on that enum. (An earlier version of this
522/// comment asserted that `OutboundDelegateMsg` already carried the attribute.
523/// It never has.)
524///
525/// # Wire format and compatibility
526///
527/// bincode, variant index 0..=N in **declaration order**. Two rules follow, and
528/// the compiler enforces neither:
529///
530/// - **Never insert or reorder a variant.** That silently reassigns every later
531/// tag, so delegate WASM compiled against an older stdlib decodes the same
532/// bytes into a *different* variant — no error, just a message quietly
533/// reinterpreted as another one. `delegate_msg_variant_tags_are_pinned` pins
534/// the tag of every variant of both enums so a reorder fails CI instead.
535/// - **Appending is compatible in exactly one direction.** An old sender's old
536/// variant always decodes on a new receiver. A **new** sender's **new**
537/// variant does **not** decode on an old receiver: bincode rejects the
538/// unknown tag — as `ErrorKind::Custom("invalid value: integer `N`, expected
539/// variant index 0 <= i < M")`, since bincode hands the index to serde's
540/// derived visitor rather than validating it itself. (Not
541/// `InvalidTagEncoding`, which bincode only ever produces for a bad `Option`
542/// discriminant.) `#[non_exhaustive]` does
543/// not change this — it is a source-level attribute with no effect on the
544/// encoding, and serde has no unknown-variant fallback to fall back to.
545///
546/// For this enum the incompatible direction is a **new host → old delegate**,
547/// and it is mostly unreachable in practice: the host emits a response variant
548/// only in reply to the matching request variant, so a delegate that never
549/// emits a request added in stdlib version X never receives the response added
550/// in X. Deployed delegate WASM therefore keeps working against an upgraded
551/// node. The genuinely constrained direction is delegate → host; see
552/// [`OutboundDelegateMsg`].
553///
554/// The compatibility claims above are asserted, not merely asserted-in-prose,
555/// by the `delegate_wire_compat` test module at the bottom of this file.
556#[non_exhaustive]
557#[derive(Serialize, Deserialize, Debug, Clone)]
558pub enum InboundDelegateMsg<'a> {
559 ApplicationMessage(ApplicationMessage),
560 UserResponse(#[serde(borrow)] UserInputResponse<'a>),
561 GetContractResponse(GetContractResponse),
562 PutContractResponse(PutContractResponse),
563 UpdateContractResponse(UpdateContractResponse),
564 SubscribeContractResponse(SubscribeContractResponse),
565 ContractNotification(ContractNotification),
566 DelegateMessage(DelegateMessage),
567 // Appended in 0.10.0 at tag 8. New variants go at the END, never inserted —
568 // see the wire-format note on this enum.
569 UnsubscribeContractResponse(UnsubscribeContractResponse),
570 /// Delivered by the host when a periodic wake-up the delegate declared in
571 /// its manifest falls due (see
572 /// [`WakeupSchedule`](crate::prelude::WakeupSchedule) and
573 /// `#[delegate(manifest(wakeups = [tag = seconds]))]`). `tag` is the
574 /// declared tag's bytes, so a delegate with several schedules can tell
575 /// them apart. Owned (`'static`).
576 ///
577 /// # History: why the request half is a manifest entry
578 ///
579 /// This variant (tag 9) shipped in 0.10.0 with a run-time request half,
580 /// `DelegateCtx::schedule_wakeup`, backed by the host import
581 /// `__frnt__delegate__schedule_wakeup`. No released freenet-core ever
582 /// provided that import, and a delegate that imports a function the node
583 /// does not provide fails to INSTANTIATE, so 0.11.0 removed it (pinned by
584 /// `host_imports`'s `the_imports_removed_in_0_11_0_have_not_come_back`).
585 ///
586 /// 0.12.1 restores the feature with the request in the manifest instead.
587 /// That is not a stylistic choice: a manifest field is ignored by a node
588 /// that does not know it, so ONE delegate build loads everywhere and
589 /// simply receives no `WakeupFired` on a node without the feature. An
590 /// import, or a new `OutboundDelegateMsg` variant (older nodes fail to
591 /// decode the whole outbound batch it sits in), would make that same build
592 /// dead or lossy on every node that predates the host side.
593 ///
594 /// A delegate only receives this if its manifest declares a wake-up, which
595 /// requires a stdlib that defines this variant, so no deployed delegate
596 /// can be sent a tag it cannot decode.
597 ///
598 /// # What the context cache holds during a wakeup
599 ///
600 /// Nothing the delegate should read. freenet-core's delegate context cache
601 /// is keyed **per delegate**, not per conversation, and entries are pruned
602 /// after `DELEGATE_CONTEXT_TTL` (10 minutes). Two consequences, both
603 /// arguing the same way:
604 ///
605 /// - A wake-up is periodic and not tied to any one exchange, so whatever
606 /// context exists when it fires belongs to something else or has
607 /// expired; with intervals of 10 minutes or more it is simply **gone**.
608 /// - If the delegate happens to have a live context from some *other*
609 /// in-flight exchange inside that window, it belongs to that exchange.
610 /// Reading it during a wakeup would be reading another conversation's
611 /// working state.
612 ///
613 /// This is why the variant carries no `DelegateContext`: there is no
614 /// coherent value to put in it. A delegate needing state across a wakeup
615 /// reads it from its secrets, which is what core's own cache doc
616 /// recommends for exactly this case.
617 ///
618 /// Appended at tag **9**, after `UnsubscribeContractResponse` at tag 8.
619 WakeupFired {
620 tag: Vec<u8>,
621 },
622 /// A lifecycle event: the delegate was installed on this node, or the
623 /// node started. See [`LifecycleEvent`](crate::prelude::LifecycleEvent).
624 ///
625 /// # What keeps this safe for deployed delegates
626 ///
627 /// A delegate built against an older stdlib cannot decode this tag (see
628 /// the wire-format note on this enum). The host sends it **only** to a
629 /// delegate whose embedded [`DelegateManifest`](crate::prelude::DelegateManifest)
630 /// lists the event's [`LifecycleKind`](crate::prelude::LifecycleKind), and
631 /// the manifest macro only lists kinds the delegate's own stdlib defines
632 /// (it names each one through the stdlib, so anything else fails to
633 /// compile). The first half is a property of the host implementation, not
634 /// of the format; freenet-core pins it.
635 ///
636 /// Carries no `DelegateContext`, for the same reason as `WakeupFired`: it
637 /// opens a conversation rather than continuing one.
638 ///
639 /// A client cannot send this variant: the host delivers it itself and
640 /// refuses it in a client's `ApplicationMessages`.
641 ///
642 /// Appended at tag **10**, after `WakeupFired` at tag 9.
643 Lifecycle(crate::delegate_manifest::LifecycleEvent),
644}
645
646impl InboundDelegateMsg<'_> {
647 pub fn into_owned(self) -> InboundDelegateMsg<'static> {
648 match self {
649 InboundDelegateMsg::ApplicationMessage(r) => InboundDelegateMsg::ApplicationMessage(r),
650 InboundDelegateMsg::UserResponse(r) => InboundDelegateMsg::UserResponse(r.into_owned()),
651 InboundDelegateMsg::GetContractResponse(r) => {
652 InboundDelegateMsg::GetContractResponse(r)
653 }
654 InboundDelegateMsg::PutContractResponse(r) => {
655 InboundDelegateMsg::PutContractResponse(r)
656 }
657 InboundDelegateMsg::UpdateContractResponse(r) => {
658 InboundDelegateMsg::UpdateContractResponse(r)
659 }
660 InboundDelegateMsg::SubscribeContractResponse(r) => {
661 InboundDelegateMsg::SubscribeContractResponse(r)
662 }
663 InboundDelegateMsg::ContractNotification(r) => {
664 InboundDelegateMsg::ContractNotification(r)
665 }
666 InboundDelegateMsg::DelegateMessage(r) => InboundDelegateMsg::DelegateMessage(r),
667 InboundDelegateMsg::UnsubscribeContractResponse(r) => {
668 InboundDelegateMsg::UnsubscribeContractResponse(r)
669 }
670 InboundDelegateMsg::WakeupFired { tag } => InboundDelegateMsg::WakeupFired { tag },
671 InboundDelegateMsg::Lifecycle(e) => InboundDelegateMsg::Lifecycle(e),
672 }
673 }
674
675 pub fn get_context(&self) -> Option<&DelegateContext> {
676 match self {
677 InboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
678 Some(context)
679 }
680 // UserResponse carries a context too. It was missing from both
681 // accessors, so this returned None for it — the `_ => None`
682 // wildcard below swallowed the omission silently. Found in review.
683 InboundDelegateMsg::UserResponse(UserInputResponse { context, .. }) => Some(context),
684 InboundDelegateMsg::GetContractResponse(GetContractResponse { context, .. }) => {
685 Some(context)
686 }
687 InboundDelegateMsg::PutContractResponse(PutContractResponse { context, .. }) => {
688 Some(context)
689 }
690 InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
691 context, ..
692 }) => Some(context),
693 InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
694 context,
695 ..
696 }) => Some(context),
697 InboundDelegateMsg::ContractNotification(ContractNotification { context, .. }) => {
698 Some(context)
699 }
700 InboundDelegateMsg::DelegateMessage(DelegateMessage { context, .. }) => Some(context),
701 InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
702 context,
703 ..
704 }) => Some(context),
705 // `WakeupFired` carries no `DelegateContext`, so `None` here is
706 // the honest answer rather than a missing arm. The reasoning lives
707 // on the variant itself -- see `InboundDelegateMsg::WakeupFired`,
708 // which explains both why a wakeup is not a reply and why the
709 // context cache could not supply a coherent value anyway. Kept in
710 // one place deliberately: a maintainer editing this accessor should
711 // not meet a second, older version of the argument.
712 InboundDelegateMsg::WakeupFired { .. } => None,
713 // `Lifecycle` carries no context either; see the variant.
714 InboundDelegateMsg::Lifecycle(_) => None,
715 // No wildcard, deliberately. The `_ => None` that used to sit here
716 // is what let UserResponse go unhandled and silently report "no
717 // context". Exhaustive means a new variant is a compile error here
718 // instead — which is how `WakeupFired` above came to be considered
719 // explicitly rather than defaulting into the wildcard.
720 //
721 // Correcting a premise this crate briefly asserted: "every variant
722 // carries a context" was already false before `WakeupFired`, and
723 // false about *this accessor* rather than about the structs. In
724 // 0.8.5 this match listed seven variants, omitted `UserResponse`
725 // — which does have a context field — and ended in `_ => None`. So
726 // the claim was true of the types and wrong about the code. That
727 // is why the `WakeupFired` exemption in the test asserts
728 // `get_context()` is `None`: it pins what this function does, not
729 // what the struct definitions look like.
730 }
731 }
732
733 pub fn get_mut_context(&mut self) -> Option<&mut DelegateContext> {
734 match self {
735 InboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
736 Some(context)
737 }
738 // UserResponse carries a context too. It was missing from both
739 // accessors, so this returned None for it — the `_ => None`
740 // wildcard below swallowed the omission silently. Found in review.
741 InboundDelegateMsg::UserResponse(UserInputResponse { context, .. }) => Some(context),
742 InboundDelegateMsg::GetContractResponse(GetContractResponse { context, .. }) => {
743 Some(context)
744 }
745 InboundDelegateMsg::PutContractResponse(PutContractResponse { context, .. }) => {
746 Some(context)
747 }
748 InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
749 context, ..
750 }) => Some(context),
751 InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
752 context,
753 ..
754 }) => Some(context),
755 InboundDelegateMsg::ContractNotification(ContractNotification { context, .. }) => {
756 Some(context)
757 }
758 InboundDelegateMsg::DelegateMessage(DelegateMessage { context, .. }) => Some(context),
759 InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
760 context,
761 ..
762 }) => Some(context),
763 // `WakeupFired` and `Lifecycle` carry no context; see `get_context`.
764 InboundDelegateMsg::WakeupFired { .. } => None,
765 InboundDelegateMsg::Lifecycle(_) => None,
766 // No wildcard, deliberately. The `_ => None` that used to sit here
767 // is what let UserResponse go unhandled and silently report "no
768 // context". Exhaustive means a new variant is a compile error here
769 // instead.
770 }
771 }
772}
773
774impl From<ApplicationMessage> for InboundDelegateMsg<'_> {
775 fn from(value: ApplicationMessage) -> Self {
776 Self::ApplicationMessage(value)
777 }
778}
779
780impl<'a> TryFromFbs<&FbsInboundDelegateMsg<'a>> for InboundDelegateMsg<'a> {
781 fn try_decode_fbs(msg: &FbsInboundDelegateMsg<'a>) -> Result<Self, WsApiError> {
782 match msg.inbound_type() {
783 InboundDelegateMsgType::common_ApplicationMessage => {
784 let app_msg = msg.inbound_as_common_application_message().unwrap();
785 let app_msg = ApplicationMessage {
786 payload: app_msg.payload().bytes().to_vec(),
787 context: DelegateContext::new(app_msg.context().bytes().to_vec()),
788 processed: app_msg.processed(),
789 };
790 Ok(InboundDelegateMsg::ApplicationMessage(app_msg))
791 }
792 InboundDelegateMsgType::UserInputResponse => {
793 let user_response = msg.inbound_as_user_input_response().unwrap();
794 let user_response = UserInputResponse {
795 request_id: user_response.request_id(),
796 response: ClientResponse::new(user_response.response().data().bytes().to_vec()),
797 context: DelegateContext::new(
798 user_response.delegate_context().bytes().to_vec(),
799 ),
800 };
801 Ok(InboundDelegateMsg::UserResponse(user_response))
802 }
803 // Reachable, not `unreachable!()`: the generated verifier for this
804 // union ends in `_ => Ok(())`, so any discriminant a client sets —
805 // including `NONE` — arrives here. See `unknown_union_discriminant`.
806 other => Err(unknown_union_discriminant(
807 "InboundDelegateMsgType",
808 other.0,
809 )),
810 }
811 }
812}
813
814#[non_exhaustive]
815#[derive(Serialize, Deserialize, Debug, Clone)]
816pub struct ApplicationMessage {
817 pub payload: Vec<u8>,
818 pub context: DelegateContext,
819 pub processed: bool,
820}
821
822impl ApplicationMessage {
823 pub fn new(payload: Vec<u8>) -> Self {
824 Self {
825 payload,
826 context: DelegateContext::default(),
827 processed: false,
828 }
829 }
830
831 pub fn with_context(mut self, context: DelegateContext) -> Self {
832 self.context = context;
833 self
834 }
835
836 pub fn processed(mut self, p: bool) -> Self {
837 self.processed = p;
838 self
839 }
840}
841
842#[derive(Serialize, Deserialize, Debug, Clone)]
843pub struct UserInputResponse<'a> {
844 pub request_id: u32,
845 #[serde(borrow)]
846 pub response: ClientResponse<'a>,
847 pub context: DelegateContext,
848}
849
850impl UserInputResponse<'_> {
851 pub fn into_owned(self) -> UserInputResponse<'static> {
852 UserInputResponse {
853 request_id: self.request_id,
854 response: self.response.into_owned(),
855 context: self.context,
856 }
857 }
858}
859
860/// Messages emitted **out of** a delegate's `process()` function.
861///
862/// This is the outbound counterpart of [`InboundDelegateMsg`] and sits on the
863/// same host↔delegate wire boundary.
864///
865/// # Deliberately not `#[non_exhaustive]`
866///
867/// Adding a variant here is a source-level break for any downstream crate that
868/// matches on it exhaustively. That is the intended behaviour and it should not
869/// be "fixed" by marking the enum.
870///
871/// Every variant of this enum is a **request the host must act on**. There is
872/// one host — freenet-core — and it dispatches these in exhaustive matches with
873/// no wildcard (`crates/core/src/contract.rs`, in the request loop and again in
874/// the app-message filter). Marking this enum `#[non_exhaustive]` would force
875/// those matches to grow `_ =>` arms, and a newly added variant would then
876/// compile against the host with **no arm of its own**: the delegate's request
877/// would fall into the wildcard, the call would appear to succeed, and nothing
878/// would report that it did nothing.
879///
880/// The compile error is what stops that, and it is the only mechanism that
881/// does. Keep it.
882///
883/// Two honest limits on this argument, because it is easy to claim more:
884///
885/// - **It forces an arm to exist, not a handler to be correct.** This crate's
886/// own FlatBuffers encoder (`client_api::client_events`) has explicit arms
887/// for six outbound variants that log an error and drop the message. The
888/// compile error made someone write those arms deliberately; it could not
889/// make them do anything useful.
890/// - **It is not the bug behind this workstream.** A delegate
891/// `SubscribeContractRequest` *is* handled by the host today. Its defect is
892/// different and subtler: it registers no demand in the network, so the
893/// subscription does not pin the contract (freenet-core#4669). Do not read
894/// the compile-error argument as a fix for that; it is a guard against a
895/// different failure that has not happened yet, which is the point of a
896/// guard.
897///
898/// [`InboundDelegateMsg`] carries the opposite trade-off, and is marked: its
899/// consumers are third-party delegate WASM, which can reasonably ignore a
900/// variant it does not know about.
901///
902/// # Wire format and compatibility
903///
904/// bincode, variant index 0..=N in **declaration order**. Never insert or
905/// reorder a variant: that silently reassigns every later tag, and deployed
906/// delegate WASM built against an older stdlib would encode into what the host
907/// now reads as a different variant. `delegate_msg_variant_tags_are_pinned`
908/// pins every tag so a reorder fails CI rather than production.
909///
910/// Appending is compatible in one direction only, and this enum is the
911/// direction that bites:
912///
913/// - **Old delegate → new host: fine, for appended VARIANTS.** The host
914/// understands every tag an older delegate can emit, so deployed delegate
915/// WASM keeps working against an upgraded node with no rebuild. This does
916/// **not** extend to appending a FIELD to an existing variant's payload
917/// struct, because a field breaks in the opposite direction. See
918/// `struct_field_wire_compat` in `client_api::client_events`.
919/// (`ApplicationMessage` is `#[non_exhaustive]`, which invites precisely that
920/// edit. It is the only payload struct here that is.)
921/// - **New delegate → old host: fails, and fails loudly.** bincode rejects the
922/// unknown variant tag — as `ErrorKind::Custom("invalid value: integer `N`,
923/// expected variant index 0 <= i < M")`, since it hands the index to serde's
924/// derived visitor rather than validating it itself — so the host surfaces a
925/// decode error on that message rather than misreading it.
926///
927/// There is deliberately **no feature-detection handshake**. A delegate cannot
928/// ask the host which variants it understands, and adding a probe would itself
929/// be a wire change with the same bootstrapping problem. The rule is therefore
930/// the blunt one: **a delegate that emits a variant introduced in stdlib
931/// version X requires a host built against stdlib >= X.**
932///
933/// Where a host function exists for the same capability, it is the better
934/// choice against older hosts. Host functions are resolved **by name at module
935/// instantiation**, so an import an old host does not provide fails at load
936/// time with a named missing-import error, instead of mid-protocol on a decode.
937///
938/// That said, the `freenet_delegate_contracts` namespace holds only
939/// `get_contract_state(_len)` — a local read. There is no host function for
940/// writing or subscribing, so `PutContractRequest`, `UpdateContractRequest` and
941/// `SubscribeContractRequest` below are the only route for those, and the
942/// variant rule above governs them.
943#[derive(Serialize, Deserialize, Debug, Clone)]
944pub enum OutboundDelegateMsg {
945 // for the apps
946 ApplicationMessage(ApplicationMessage),
947 RequestUserInput(
948 #[serde(deserialize_with = "OutboundDelegateMsg::deser_user_input_req")]
949 UserInputRequest<'static>,
950 ),
951 // todo: remove when context can be accessed from the delegate environment and we pass it as reference
952 ContextUpdated(DelegateContext),
953 GetContractRequest(GetContractRequest),
954 PutContractRequest(PutContractRequest),
955 UpdateContractRequest(UpdateContractRequest),
956 SubscribeContractRequest(SubscribeContractRequest),
957 SendDelegateMessage(DelegateMessage),
958 // Appended in 0.10.0 at tag 8. New variants go at the END, never inserted —
959 // see the wire-format note on this enum.
960 UnsubscribeContractRequest(UnsubscribeContractRequest),
961}
962
963impl From<ApplicationMessage> for OutboundDelegateMsg {
964 fn from(req: ApplicationMessage) -> Self {
965 Self::ApplicationMessage(req)
966 }
967}
968
969impl From<GetContractRequest> for OutboundDelegateMsg {
970 fn from(req: GetContractRequest) -> Self {
971 Self::GetContractRequest(req)
972 }
973}
974
975impl From<PutContractRequest> for OutboundDelegateMsg {
976 fn from(req: PutContractRequest) -> Self {
977 Self::PutContractRequest(req)
978 }
979}
980
981impl From<UpdateContractRequest> for OutboundDelegateMsg {
982 fn from(req: UpdateContractRequest) -> Self {
983 Self::UpdateContractRequest(req)
984 }
985}
986
987impl From<SubscribeContractRequest> for OutboundDelegateMsg {
988 fn from(req: SubscribeContractRequest) -> Self {
989 Self::SubscribeContractRequest(req)
990 }
991}
992
993impl From<UnsubscribeContractRequest> for OutboundDelegateMsg {
994 fn from(req: UnsubscribeContractRequest) -> Self {
995 Self::UnsubscribeContractRequest(req)
996 }
997}
998
999impl From<DelegateMessage> for OutboundDelegateMsg {
1000 fn from(msg: DelegateMessage) -> Self {
1001 Self::SendDelegateMessage(msg)
1002 }
1003}
1004
1005impl OutboundDelegateMsg {
1006 fn deser_user_input_req<'de, D>(deser: D) -> Result<UserInputRequest<'static>, D::Error>
1007 where
1008 D: serde::Deserializer<'de>,
1009 {
1010 let value = <UserInputRequest<'de> as Deserialize>::deserialize(deser)?;
1011 Ok(value.into_owned())
1012 }
1013
1014 pub fn processed(&self) -> bool {
1015 match self {
1016 OutboundDelegateMsg::ApplicationMessage(msg) => msg.processed,
1017 OutboundDelegateMsg::GetContractRequest(msg) => msg.processed,
1018 OutboundDelegateMsg::PutContractRequest(msg) => msg.processed,
1019 OutboundDelegateMsg::UpdateContractRequest(msg) => msg.processed,
1020 OutboundDelegateMsg::SubscribeContractRequest(msg) => msg.processed,
1021 OutboundDelegateMsg::UnsubscribeContractRequest(msg) => msg.processed,
1022 OutboundDelegateMsg::SendDelegateMessage(msg) => msg.processed,
1023 OutboundDelegateMsg::RequestUserInput(_) => true,
1024 OutboundDelegateMsg::ContextUpdated(_) => true,
1025 }
1026 }
1027
1028 pub fn get_context(&self) -> Option<&DelegateContext> {
1029 match self {
1030 OutboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
1031 Some(context)
1032 }
1033 OutboundDelegateMsg::GetContractRequest(GetContractRequest { context, .. }) => {
1034 Some(context)
1035 }
1036 OutboundDelegateMsg::PutContractRequest(PutContractRequest { context, .. }) => {
1037 Some(context)
1038 }
1039 OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
1040 context, ..
1041 }) => Some(context),
1042 OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
1043 context,
1044 ..
1045 }) => Some(context),
1046 OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest {
1047 context,
1048 ..
1049 }) => Some(context),
1050 OutboundDelegateMsg::SendDelegateMessage(DelegateMessage { context, .. }) => {
1051 Some(context)
1052 }
1053 _ => None,
1054 }
1055 }
1056
1057 pub fn get_mut_context(&mut self) -> Option<&mut DelegateContext> {
1058 match self {
1059 OutboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
1060 Some(context)
1061 }
1062 OutboundDelegateMsg::GetContractRequest(GetContractRequest { context, .. }) => {
1063 Some(context)
1064 }
1065 OutboundDelegateMsg::PutContractRequest(PutContractRequest { context, .. }) => {
1066 Some(context)
1067 }
1068 OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
1069 context, ..
1070 }) => Some(context),
1071 OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
1072 context,
1073 ..
1074 }) => Some(context),
1075 OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest {
1076 context,
1077 ..
1078 }) => Some(context),
1079 OutboundDelegateMsg::SendDelegateMessage(DelegateMessage { context, .. }) => {
1080 Some(context)
1081 }
1082 _ => None,
1083 }
1084 }
1085}
1086
1087/// Request to get contract state from within a delegate.
1088#[derive(Serialize, Deserialize, Debug, Clone)]
1089pub struct GetContractRequest {
1090 pub contract_id: ContractInstanceId,
1091 pub context: DelegateContext,
1092 pub processed: bool,
1093}
1094
1095impl GetContractRequest {
1096 pub fn new(contract_id: ContractInstanceId) -> Self {
1097 Self {
1098 contract_id,
1099 context: Default::default(),
1100 processed: false,
1101 }
1102 }
1103}
1104
1105/// Response containing contract state for a delegate.
1106#[derive(Serialize, Deserialize, Debug, Clone)]
1107pub struct GetContractResponse {
1108 pub contract_id: ContractInstanceId,
1109 /// The contract state, or None if the contract was not found locally.
1110 pub state: Option<WrappedState>,
1111 pub context: DelegateContext,
1112}
1113
1114/// Request to store a new contract from within a delegate.
1115#[derive(Serialize, Deserialize, Debug, Clone)]
1116pub struct PutContractRequest {
1117 /// The contract code and parameters.
1118 pub contract: ContractContainer,
1119 /// The initial state for the contract.
1120 pub state: WrappedState,
1121 /// Related contracts that this contract depends on.
1122 #[serde(deserialize_with = "RelatedContracts::deser_related_contracts")]
1123 pub related_contracts: RelatedContracts<'static>,
1124 /// Context for the delegate.
1125 pub context: DelegateContext,
1126 /// Whether this request has been processed.
1127 pub processed: bool,
1128}
1129
1130impl PutContractRequest {
1131 pub fn new(
1132 contract: ContractContainer,
1133 state: WrappedState,
1134 related_contracts: RelatedContracts<'static>,
1135 ) -> Self {
1136 Self {
1137 contract,
1138 state,
1139 related_contracts,
1140 context: Default::default(),
1141 processed: false,
1142 }
1143 }
1144}
1145
1146/// Response after attempting to store a contract from a delegate.
1147#[derive(Serialize, Deserialize, Debug, Clone)]
1148pub struct PutContractResponse {
1149 /// The ID of the contract that was (attempted to be) stored.
1150 pub contract_id: ContractInstanceId,
1151 /// Success (Ok) or error message (Err).
1152 pub result: Result<(), String>,
1153 /// Context for the delegate.
1154 pub context: DelegateContext,
1155}
1156
1157/// Request to update an existing contract's state from within a delegate.
1158#[derive(Serialize, Deserialize, Debug, Clone)]
1159pub struct UpdateContractRequest {
1160 /// The contract to update.
1161 pub contract_id: ContractInstanceId,
1162 /// The update to apply (full state or delta).
1163 #[serde(deserialize_with = "UpdateContractRequest::deser_update_data")]
1164 pub update: UpdateData<'static>,
1165 /// Context for the delegate.
1166 pub context: DelegateContext,
1167 /// Whether this request has been processed.
1168 pub processed: bool,
1169}
1170
1171impl UpdateContractRequest {
1172 pub fn new(contract_id: ContractInstanceId, update: UpdateData<'static>) -> Self {
1173 Self {
1174 contract_id,
1175 update,
1176 context: Default::default(),
1177 processed: false,
1178 }
1179 }
1180
1181 fn deser_update_data<'de, D>(deser: D) -> Result<UpdateData<'static>, D::Error>
1182 where
1183 D: Deserializer<'de>,
1184 {
1185 let value = <UpdateData<'de> as Deserialize>::deserialize(deser)?;
1186 Ok(value.into_owned())
1187 }
1188}
1189
1190/// Response after attempting to update a contract from a delegate.
1191#[derive(Serialize, Deserialize, Debug, Clone)]
1192pub struct UpdateContractResponse {
1193 /// The contract that was updated.
1194 pub contract_id: ContractInstanceId,
1195 /// Success (Ok) or error message (Err).
1196 pub result: Result<(), String>,
1197 /// Context for the delegate.
1198 pub context: DelegateContext,
1199}
1200
1201/// Request to subscribe to a contract's state changes from within a delegate.
1202#[derive(Serialize, Deserialize, Debug, Clone)]
1203pub struct SubscribeContractRequest {
1204 /// The contract to subscribe to.
1205 pub contract_id: ContractInstanceId,
1206 /// Context for the delegate.
1207 pub context: DelegateContext,
1208 /// Whether this request has been processed.
1209 pub processed: bool,
1210}
1211
1212impl SubscribeContractRequest {
1213 pub fn new(contract_id: ContractInstanceId) -> Self {
1214 Self {
1215 contract_id,
1216 context: Default::default(),
1217 processed: false,
1218 }
1219 }
1220}
1221
1222/// Response after attempting to subscribe to a contract from a delegate.
1223#[derive(Serialize, Deserialize, Debug, Clone)]
1224pub struct SubscribeContractResponse {
1225 /// The contract subscribed to.
1226 pub contract_id: ContractInstanceId,
1227 /// Success (Ok) or error message (Err).
1228 pub result: Result<(), String>,
1229 /// Context for the delegate.
1230 pub context: DelegateContext,
1231}
1232
1233/// Request to stop receiving a contract's state changes, from within a delegate.
1234///
1235/// The counterpart of [`SubscribeContractRequest`]. Before 0.10.0 a delegate had
1236/// no way to drop a subscription it had taken: the only release path was the
1237/// implicit cleanup when the delegate itself was unregistered, so a delegate
1238/// that had finished with a contract went on holding interest in it for as long
1239/// as the delegate existed. Specified in freenet-core#2830 alongside subscribe;
1240/// only subscribe was built.
1241///
1242/// Answered with [`InboundDelegateMsg::UnsubscribeContractResponse`].
1243///
1244/// Field order is the wire format. Do not reorder.
1245#[derive(Serialize, Deserialize, Debug, Clone)]
1246pub struct UnsubscribeContractRequest {
1247 /// The contract to stop receiving notifications for.
1248 pub contract_id: ContractInstanceId,
1249 /// Context for the delegate.
1250 pub context: DelegateContext,
1251 /// Whether this request has been processed.
1252 pub processed: bool,
1253}
1254
1255impl UnsubscribeContractRequest {
1256 pub fn new(contract_id: ContractInstanceId) -> Self {
1257 Self {
1258 contract_id,
1259 context: Default::default(),
1260 processed: false,
1261 }
1262 }
1263}
1264
1265/// Response after attempting to unsubscribe from a contract from a delegate.
1266///
1267/// **Unsubscribing a contract the delegate is not subscribed to reports
1268/// `Ok(())`, not an error.** That is not a convenience: it is what the host
1269/// actually does. Teardown goes through the same removal path that a
1270/// no-longer-present client id already takes as a no-op, so returning an error
1271/// would have the host inventing a failure it did not have. It also matches the
1272/// subscribe side, where a repeat subscribe is a set insert.
1273///
1274/// Field order is the wire format. Do not reorder.
1275#[derive(Serialize, Deserialize, Debug, Clone)]
1276pub struct UnsubscribeContractResponse {
1277 /// The contract unsubscribed from.
1278 pub contract_id: ContractInstanceId,
1279 /// Success (Ok) or error message (Err). Unsubscribing a contract the
1280 /// delegate was not subscribed to reports `Ok(())`.
1281 pub result: Result<(), String>,
1282 /// Context for the delegate.
1283 pub context: DelegateContext,
1284}
1285
1286/// A message sent from one delegate to another.
1287///
1288/// Delegates can communicate with each other by emitting
1289/// `OutboundDelegateMsg::SendDelegateMessage` with a `DelegateMessage` targeting
1290/// another delegate. The runtime delivers it as `InboundDelegateMsg::DelegateMessage`
1291/// to the target delegate's `process()` function.
1292///
1293/// The `sender` field is overwritten by the runtime with the actual sender's key
1294/// (sender attestation), so delegates cannot spoof their identity.
1295#[derive(Serialize, Deserialize, Debug, Clone)]
1296pub struct DelegateMessage {
1297 /// The delegate to deliver this message to.
1298 pub target: DelegateKey,
1299 /// The delegate that sent this message (overwritten by runtime for attestation).
1300 pub sender: DelegateKey,
1301 /// Arbitrary message payload.
1302 pub payload: Vec<u8>,
1303 /// Delegate context, carried through the processing pipeline.
1304 pub context: DelegateContext,
1305 /// Runtime protocol flag indicating whether this message has been delivered.
1306 pub processed: bool,
1307}
1308
1309impl DelegateMessage {
1310 pub fn new(target: DelegateKey, sender: DelegateKey, payload: Vec<u8>) -> Self {
1311 Self {
1312 target,
1313 sender,
1314 payload,
1315 context: DelegateContext::default(),
1316 processed: false,
1317 }
1318 }
1319}
1320
1321/// Notification delivered to a delegate when a subscribed contract's state changes.
1322#[derive(Serialize, Deserialize, Debug, Clone)]
1323pub struct ContractNotification {
1324 /// The contract whose state changed.
1325 pub contract_id: ContractInstanceId,
1326 /// The new state of the contract.
1327 pub new_state: WrappedState,
1328 /// Context for the delegate.
1329 pub context: DelegateContext,
1330}
1331
1332#[serde_as]
1333#[derive(Serialize, Deserialize, Debug, Clone)]
1334pub struct NotificationMessage<'a>(
1335 #[serde_as(as = "serde_with::Bytes")]
1336 #[serde(borrow)]
1337 Cow<'a, [u8]>,
1338);
1339
1340impl TryFrom<&serde_json::Value> for NotificationMessage<'static> {
1341 type Error = ();
1342
1343 fn try_from(json: &serde_json::Value) -> Result<NotificationMessage<'static>, ()> {
1344 // todo: validate format when we have a better idea of what we want here
1345 let bytes = serde_json::to_vec(json).unwrap();
1346 Ok(Self(Cow::Owned(bytes)))
1347 }
1348}
1349
1350impl NotificationMessage<'_> {
1351 pub fn into_owned(self) -> NotificationMessage<'static> {
1352 NotificationMessage(self.0.into_owned().into())
1353 }
1354 pub fn bytes(&self) -> &[u8] {
1355 self.0.as_ref()
1356 }
1357}
1358
1359#[serde_as]
1360#[derive(Serialize, Deserialize, Debug, Clone)]
1361pub struct ClientResponse<'a>(
1362 #[serde_as(as = "serde_with::Bytes")]
1363 #[serde(borrow)]
1364 Cow<'a, [u8]>,
1365);
1366
1367impl Deref for ClientResponse<'_> {
1368 type Target = [u8];
1369
1370 fn deref(&self) -> &Self::Target {
1371 &self.0
1372 }
1373}
1374
1375impl ClientResponse<'_> {
1376 pub fn new(response: Vec<u8>) -> Self {
1377 Self(response.into())
1378 }
1379 pub fn into_owned(self) -> ClientResponse<'static> {
1380 ClientResponse(self.0.into_owned().into())
1381 }
1382 pub fn bytes(&self) -> &[u8] {
1383 self.0.as_ref()
1384 }
1385}
1386
1387#[derive(Serialize, Deserialize, Debug, Clone)]
1388pub struct UserInputRequest<'a> {
1389 pub request_id: u32,
1390 #[serde(borrow)]
1391 /// An interpretable message by the notification system.
1392 pub message: NotificationMessage<'a>,
1393 /// If a response is required from the user they can be chosen from this list.
1394 pub responses: Vec<ClientResponse<'a>>,
1395}
1396
1397impl UserInputRequest<'_> {
1398 pub fn into_owned(self) -> UserInputRequest<'static> {
1399 UserInputRequest {
1400 request_id: self.request_id,
1401 message: self.message.into_owned(),
1402 responses: self.responses.into_iter().map(|r| r.into_owned()).collect(),
1403 }
1404 }
1405}
1406
1407#[doc(hidden)]
1408pub(crate) mod wasm_interface {
1409 //! Contains all the types to interface between the host environment and
1410 //! the wasm module execution.
1411 use super::*;
1412 use crate::memory::WasmLinearMem;
1413
1414 #[repr(C)]
1415 #[derive(Debug, Clone, Copy)]
1416 pub struct DelegateInterfaceResult {
1417 ptr: i64,
1418 size: u32,
1419 }
1420
1421 impl DelegateInterfaceResult {
1422 pub unsafe fn from_raw(ptr: i64, mem: &WasmLinearMem) -> Self {
1423 let result = Box::leak(Box::from_raw(crate::memory::buf::compute_ptr(
1424 ptr as *mut Self,
1425 mem,
1426 )));
1427 #[cfg(feature = "trace")]
1428 {
1429 tracing::trace!(
1430 "got FFI result @ {ptr} ({:p}) -> {result:?}",
1431 ptr as *mut Self
1432 );
1433 }
1434 *result
1435 }
1436
1437 #[cfg(feature = "contract")]
1438 pub fn into_raw(self) -> i64 {
1439 #[cfg(feature = "trace")]
1440 {
1441 tracing::trace!("returning FFI -> {self:?}");
1442 }
1443 let ptr = Box::into_raw(Box::new(self));
1444 #[cfg(feature = "trace")]
1445 {
1446 tracing::trace!("FFI result ptr: {ptr:p} ({}i64)", ptr as i64);
1447 }
1448 ptr as _
1449 }
1450
1451 pub unsafe fn unwrap(
1452 self,
1453 mem: WasmLinearMem,
1454 ) -> Result<Vec<OutboundDelegateMsg>, DelegateError> {
1455 let ptr = crate::memory::buf::compute_ptr(self.ptr as *mut u8, &mem);
1456 let serialized = std::slice::from_raw_parts(ptr as *const u8, self.size as _);
1457 let value: Result<Vec<OutboundDelegateMsg>, DelegateError> =
1458 bincode::deserialize(serialized)
1459 .map_err(|e| DelegateError::Other(format!("{e}")))?;
1460 #[cfg(feature = "trace")]
1461 {
1462 tracing::trace!(
1463 "got result through FFI; addr: {:p} ({}i64, mapped: {ptr:p})
1464 serialized: {serialized:?}
1465 value: {value:?}",
1466 self.ptr as *mut u8,
1467 self.ptr
1468 );
1469 }
1470 value
1471 }
1472 }
1473
1474 impl From<Result<Vec<OutboundDelegateMsg>, DelegateError>> for DelegateInterfaceResult {
1475 fn from(value: Result<Vec<OutboundDelegateMsg>, DelegateError>) -> Self {
1476 let serialized = bincode::serialize(&value).unwrap();
1477 let size = serialized.len() as _;
1478 let ptr = serialized.as_ptr();
1479 #[cfg(feature = "trace")]
1480 {
1481 tracing::trace!(
1482 "sending result through FFI; addr: {ptr:p} ({}),\n serialized: {serialized:?}\n value: {value:?}",
1483 ptr as i64
1484 );
1485 }
1486 std::mem::forget(serialized);
1487 Self {
1488 ptr: ptr as i64,
1489 size,
1490 }
1491 }
1492 }
1493}
1494
1495#[cfg(test)]
1496mod message_origin_tests {
1497 use super::*;
1498
1499 /// Wire-format pin: bincode encoding of `MessageOrigin::WebApp(..)` must
1500 /// stay byte-identical across stdlib releases. Deployed delegate WASM
1501 /// compiled against an older stdlib will receive these bytes from a
1502 /// host running the new stdlib and must continue to deserialize them.
1503 /// If this test ever fails, it is a wire-format break and is NOT
1504 /// publishable as a non-major bump.
1505 #[test]
1506 fn webapp_origin_wire_format_is_stable() {
1507 let id = ContractInstanceId::new([0xABu8; 32]);
1508 let origin = MessageOrigin::WebApp(id);
1509 let encoded = bincode::serialize(&origin).unwrap();
1510
1511 // Variant tag 0 (4-byte LE u32 in default bincode config) followed by
1512 // the 32 raw bytes of the ContractInstanceId.
1513 let mut expected = vec![0u8, 0, 0, 0];
1514 expected.extend_from_slice(&[0xABu8; 32]);
1515 assert_eq!(encoded, expected);
1516 }
1517
1518 /// Wire-format pin for the `Delegate` variant. Locks the full byte
1519 /// layout (variant tag + serde repr of `DelegateKey`) so that any future
1520 /// change to either `DelegateKey`'s serde or the workspace bincode
1521 /// config is caught loudly. If `DelegateKey`'s on-the-wire encoding
1522 /// changes, deployed delegates compiled against a previous stdlib will
1523 /// silently fail to deserialize inter-delegate origins — which is
1524 /// exactly the failure mode this test exists to prevent.
1525 #[test]
1526 fn delegate_origin_wire_format_is_stable() {
1527 let key = DelegateKey::new([0x11u8; 32], crate::code_hash::CodeHash::new([0x22u8; 32]));
1528 let origin = MessageOrigin::Delegate(key);
1529 let encoded = bincode::serialize(&origin).unwrap();
1530
1531 // Variant tag 1 (4-byte LE u32 in default bincode config), followed
1532 // by the 32-byte `key` field, followed by the 32-byte `code_hash`
1533 // field of `DelegateKey`.
1534 let mut expected = vec![1u8, 0, 0, 0];
1535 expected.extend_from_slice(&[0x11u8; 32]);
1536 expected.extend_from_slice(&[0x22u8; 32]);
1537 assert_eq!(encoded, expected);
1538
1539 // And it must still round-trip.
1540 let decoded: MessageOrigin = bincode::deserialize(&encoded).unwrap();
1541 assert!(matches!(decoded, MessageOrigin::Delegate(_)));
1542 }
1543
1544 /// Wire-format pin for the first variant of [`InboundDelegateMsg`]. Pins
1545 /// the tag so that reordering the enum cannot silently shift existing
1546 /// deployed delegate WASM off the correct variant. Only tag+payload
1547 /// prefix is asserted (not the full ApplicationMessage byte layout),
1548 /// since ApplicationMessage's internal fields have their own stability
1549 /// expectations handled at a different layer. What matters here is that
1550 /// variant 0 stays `ApplicationMessage` on the wire.
1551 #[test]
1552 fn inbound_delegate_msg_wire_format_is_stable() {
1553 let msg = InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC]));
1554 let encoded = bincode::serialize(&msg).unwrap();
1555 assert_eq!(
1556 encoded[..4],
1557 [0, 0, 0, 0],
1558 "ApplicationMessage must stay at variant tag 0 on the wire; \
1559 reordering InboundDelegateMsg variants is a wire-format break"
1560 );
1561 // And it must still round-trip into the same variant.
1562 let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&encoded).unwrap();
1563 assert!(matches!(decoded, InboundDelegateMsg::ApplicationMessage(_)));
1564 }
1565
1566 /// Wire-format pin for [`InboundDelegateMsg::WakeupFired`]. It is the 10th
1567 /// variant (declaration index 9), so its bincode tag must be `9` (4-byte
1568 /// LE) — it sits behind `UnsubscribeContractResponse` at tag 8. Once
1569 /// shipped this tag is frozen: reordering or inserting a variant ahead of
1570 /// it would silently redirect a host's wakeup delivery to the wrong variant
1571 /// on a delegate compiled against this stdlib.
1572 #[test]
1573 fn inbound_wakeup_fired_wire_format_is_stable() {
1574 let msg = InboundDelegateMsg::WakeupFired {
1575 tag: vec![0xAA, 0xBB],
1576 };
1577 let encoded = bincode::serialize(&msg).unwrap();
1578
1579 // tag 9 (u32 LE) + Vec<u8> len (u64 LE = 2) + the two tag bytes.
1580 let mut expected = vec![9u8, 0, 0, 0];
1581 expected.extend_from_slice(&[2, 0, 0, 0, 0, 0, 0, 0]);
1582 expected.extend_from_slice(&[0xAA, 0xBB]);
1583 assert_eq!(
1584 encoded, expected,
1585 "WakeupFired must stay at variant tag 9 with a stable payload layout"
1586 );
1587
1588 let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&encoded).unwrap();
1589 assert!(matches!(
1590 decoded,
1591 InboundDelegateMsg::WakeupFired { tag } if tag == vec![0xAA, 0xBB]
1592 ));
1593 }
1594}
1595
1596/// Executable evidence for the wire-compatibility rules documented on
1597/// [`InboundDelegateMsg`] and [`OutboundDelegateMsg`].
1598///
1599/// The claims those doc comments make about bincode's behaviour are asserted
1600/// here rather than believed, because every one of them is the kind of claim
1601/// that is easy to state, easy to get backwards, and impossible to notice being
1602/// wrong until deployed delegate WASM misreads a message in production.
1603#[cfg(test)]
1604mod delegate_wire_compat {
1605 use super::*;
1606 use crate::contract_interface::WrappedContract;
1607 use crate::prelude::ContractCode;
1608 use crate::versioning::ContractWasmAPIVersion;
1609 use std::sync::Arc;
1610
1611 /// The number of variants each enum has **today**. These are not free
1612 /// parameters: see `an_unpinned_variant_fails_this_test`, which is what
1613 /// makes them fail closed rather than drift.
1614 const INBOUND_VARIANT_COUNT: u32 = 11;
1615 const OUTBOUND_VARIANT_COUNT: u32 = 9;
1616
1617 fn instance_id() -> ContractInstanceId {
1618 ContractInstanceId::new([0x5Au8; 32])
1619 }
1620
1621 fn delegate_key() -> DelegateKey {
1622 DelegateKey::new([0x11u8; 32], CodeHash::new([0x22u8; 32]))
1623 }
1624
1625 fn contract_container() -> ContractContainer {
1626 ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new(
1627 Arc::new(ContractCode::from(vec![1u8, 2, 3])),
1628 Parameters::from(vec![9u8, 8, 7]),
1629 )))
1630 }
1631
1632 /// The bincode variant tag actually on the wire: a 4-byte little-endian
1633 /// u32 prefix (this workspace's bincode config uses fixint encoding).
1634 fn wire_tag(encoded: &[u8]) -> u32 {
1635 u32::from_le_bytes(
1636 encoded[..4]
1637 .try_into()
1638 .expect("a bincode enum encoding starts with a 4-byte tag"),
1639 )
1640 }
1641
1642 /// The tag each [`InboundDelegateMsg`] variant is frozen at, forever.
1643 ///
1644 /// This match is **exhaustive on purpose**. `#[non_exhaustive]` has no
1645 /// effect inside the crate that defines the enum, so adding a variant
1646 /// without adding an arm here is a **compile error** — which is the point.
1647 /// A new variant cannot slip in unpinned.
1648 ///
1649 /// If you are here because you added a variant: give it the next unused
1650 /// number, append it at the END of the enum, add it to `every_inbound`
1651 /// below, and bump `INBOUND_VARIANT_COUNT`. Do not renumber anything.
1652 fn pinned_inbound_tag(msg: &InboundDelegateMsg<'_>) -> u32 {
1653 match msg {
1654 InboundDelegateMsg::ApplicationMessage(_) => 0,
1655 InboundDelegateMsg::UserResponse(_) => 1,
1656 InboundDelegateMsg::GetContractResponse(_) => 2,
1657 InboundDelegateMsg::PutContractResponse(_) => 3,
1658 InboundDelegateMsg::UpdateContractResponse(_) => 4,
1659 InboundDelegateMsg::SubscribeContractResponse(_) => 5,
1660 InboundDelegateMsg::ContractNotification(_) => 6,
1661 InboundDelegateMsg::DelegateMessage(_) => 7,
1662 InboundDelegateMsg::UnsubscribeContractResponse(_) => 8,
1663 InboundDelegateMsg::WakeupFired { .. } => 9,
1664 InboundDelegateMsg::Lifecycle(_) => 10,
1665 }
1666 }
1667
1668 /// The tag each [`OutboundDelegateMsg`] variant is frozen at, forever.
1669 /// Exhaustive for the same reason as [`pinned_inbound_tag`].
1670 fn pinned_outbound_tag(msg: &OutboundDelegateMsg) -> u32 {
1671 match msg {
1672 OutboundDelegateMsg::ApplicationMessage(_) => 0,
1673 OutboundDelegateMsg::RequestUserInput(_) => 1,
1674 OutboundDelegateMsg::ContextUpdated(_) => 2,
1675 OutboundDelegateMsg::GetContractRequest(_) => 3,
1676 OutboundDelegateMsg::PutContractRequest(_) => 4,
1677 OutboundDelegateMsg::UpdateContractRequest(_) => 5,
1678 OutboundDelegateMsg::SubscribeContractRequest(_) => 6,
1679 OutboundDelegateMsg::SendDelegateMessage(_) => 7,
1680 OutboundDelegateMsg::UnsubscribeContractRequest(_) => 8,
1681 }
1682 }
1683
1684 /// One value of every [`InboundDelegateMsg`] variant.
1685 fn every_inbound() -> Vec<InboundDelegateMsg<'static>> {
1686 let id = instance_id();
1687 let ctx = DelegateContext::default();
1688 vec![
1689 InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC])),
1690 InboundDelegateMsg::UserResponse(UserInputResponse {
1691 request_id: 7,
1692 response: ClientResponse::new(vec![0x01]),
1693 context: ctx.clone(),
1694 }),
1695 InboundDelegateMsg::GetContractResponse(GetContractResponse {
1696 contract_id: id,
1697 state: None,
1698 context: ctx.clone(),
1699 }),
1700 InboundDelegateMsg::PutContractResponse(PutContractResponse {
1701 contract_id: id,
1702 result: Ok(()),
1703 context: ctx.clone(),
1704 }),
1705 InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
1706 contract_id: id,
1707 result: Ok(()),
1708 context: ctx.clone(),
1709 }),
1710 InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
1711 contract_id: id,
1712 result: Ok(()),
1713 context: ctx.clone(),
1714 }),
1715 InboundDelegateMsg::ContractNotification(ContractNotification {
1716 contract_id: id,
1717 new_state: WrappedState::new(vec![0xAB]),
1718 context: ctx.clone(),
1719 }),
1720 InboundDelegateMsg::DelegateMessage(DelegateMessage::new(
1721 delegate_key(),
1722 delegate_key(),
1723 vec![0xEE],
1724 )),
1725 InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
1726 contract_id: id,
1727 result: Ok(()),
1728 context: ctx.clone(),
1729 }),
1730 InboundDelegateMsg::WakeupFired {
1731 tag: vec![0xAA, 0xBB],
1732 },
1733 InboundDelegateMsg::Lifecycle(crate::delegate_manifest::LifecycleEvent::Installed),
1734 ]
1735 }
1736
1737 /// One value of every [`OutboundDelegateMsg`] variant.
1738 ///
1739 /// Every variant is covered, `PutContractRequest` included: building a
1740 /// `ContractContainer` is four lines (see `contract_container`), and a pin
1741 /// test with a hole in it is exactly the shape of guard that reads as
1742 /// coverage while providing none.
1743 fn every_outbound() -> Vec<OutboundDelegateMsg> {
1744 let id = instance_id();
1745 vec![
1746 OutboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC])),
1747 OutboundDelegateMsg::RequestUserInput(UserInputRequest {
1748 request_id: 7,
1749 message: NotificationMessage(Cow::Owned(vec![0x02])),
1750 responses: vec![],
1751 }),
1752 OutboundDelegateMsg::ContextUpdated(DelegateContext::default()),
1753 OutboundDelegateMsg::GetContractRequest(GetContractRequest::new(id)),
1754 OutboundDelegateMsg::PutContractRequest(PutContractRequest::new(
1755 contract_container(),
1756 WrappedState::new(vec![0xAB]),
1757 RelatedContracts::default(),
1758 )),
1759 OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest::new(
1760 id,
1761 UpdateData::State(vec![0xAB].into()),
1762 )),
1763 OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest::new(id)),
1764 OutboundDelegateMsg::SendDelegateMessage(DelegateMessage::new(
1765 delegate_key(),
1766 delegate_key(),
1767 vec![0xEE],
1768 )),
1769 OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest::new(id)),
1770 ]
1771 }
1772
1773 /// Pins the bincode variant tag of **every** variant of both delegate
1774 /// message enums.
1775 ///
1776 /// The pin this replaces covered `InboundDelegateMsg`'s variant 0 alone, so
1777 /// any reorder that happened to leave `ApplicationMessage` first — swapping
1778 /// `UserResponse` and `GetContractResponse`, say — went undetected. That is
1779 /// not a theoretical gap: exactly that swap was written, and staged, during
1780 /// the work that produced this test.
1781 ///
1782 /// A reorder is the dangerous edit precisely because it is silent. The
1783 /// bytes still decode. They decode into the wrong variant, and the failure
1784 /// surfaces as a delegate acting on a message it was never sent.
1785 ///
1786 /// **If this test fails, do not update the expected numbers.** Either a
1787 /// variant was inserted or reordered (revert it; append instead), or one
1788 /// was removed — which reassigns every later tag and is a wire break
1789 /// needing a deliberate release decision. See the
1790 /// `RegisterDelegateWithPredecessors` removal in 0.9.0 for the shape of
1791 /// that decision: it was appended last specifically so that removing it
1792 /// renumbered nothing.
1793 #[test]
1794 fn delegate_msg_variant_tags_are_pinned() {
1795 for msg in every_inbound() {
1796 let expected = pinned_inbound_tag(&msg);
1797 let encoded = bincode::serialize(&msg).expect("inbound must serialize");
1798 assert_eq!(
1799 wire_tag(&encoded),
1800 expected,
1801 "InboundDelegateMsg::{msg:?} moved off wire tag {expected}; inserting, \
1802 reordering or removing variants breaks deployed delegate WASM"
1803 );
1804 }
1805
1806 for msg in every_outbound() {
1807 let expected = pinned_outbound_tag(&msg);
1808 let encoded = bincode::serialize(&msg).expect("outbound must serialize");
1809 assert_eq!(
1810 wire_tag(&encoded),
1811 expected,
1812 "OutboundDelegateMsg::{msg:?} moved off wire tag {expected}; inserting, \
1813 reordering or removing variants breaks deployed delegate WASM"
1814 );
1815 }
1816 }
1817
1818 /// Every variant is actually exercised by the pin above.
1819 ///
1820 /// [`pinned_inbound_tag`] is exhaustive, so a new variant cannot be left
1821 /// unpinned without a compile error — but it *could* be left out of
1822 /// `every_inbound`, and then the pin would silently stop covering it.
1823 /// Asserting that the sampled tags are exactly `0..COUNT`, with no gaps and
1824 /// no repeats, closes that.
1825 #[test]
1826 fn every_variant_is_covered_by_the_pin() {
1827 let mut inbound: Vec<u32> = every_inbound().iter().map(pinned_inbound_tag).collect();
1828 inbound.sort_unstable();
1829 assert_eq!(
1830 inbound,
1831 (0..INBOUND_VARIANT_COUNT).collect::<Vec<_>>(),
1832 "every_inbound must contain each InboundDelegateMsg variant exactly once"
1833 );
1834
1835 let mut outbound: Vec<u32> = every_outbound().iter().map(pinned_outbound_tag).collect();
1836 outbound.sort_unstable();
1837 assert_eq!(
1838 outbound,
1839 (0..OUTBOUND_VARIANT_COUNT).collect::<Vec<_>>(),
1840 "every_outbound must contain each OutboundDelegateMsg variant exactly once"
1841 );
1842 }
1843
1844 /// The count constants above cannot be allowed to drift, so this probes the
1845 /// enums themselves: a payload whose tag is one past the last known variant
1846 /// must fail to decode.
1847 ///
1848 /// This is the test that fails **closed**. Add a variant and forget
1849 /// everything else here, and the tag that was previously undecodable
1850 /// becomes decodable, and this fails. Without it, `INBOUND_VARIANT_COUNT`
1851 /// would be a number asserted only against a list written by the same hand
1852 /// in the same commit — which is not a check, it is a restatement.
1853 ///
1854 /// The payload is a run of zero bytes after the tag, which decodes as
1855 /// empty vectors, `None`, `Ok`, `false` and zeroed arrays, so it satisfies
1856 /// essentially any variant shape a new variant is likely to have. Trailing
1857 /// bytes are ignored: `bincode::deserialize` configures
1858 /// `allow_trailing_bytes()` (bincode-1.3.3 `src/lib.rs`), which is also why
1859 /// a fixed-size probe is safe here.
1860 #[test]
1861 fn an_unpinned_variant_fails_this_test() {
1862 // The probe must fail because the TAG is unknown, not because a
1863 // payload of zeros happened not to parse. Asserting only `is_err()`
1864 // would let a new variant whose first field rejects zeros (a
1865 // `DateTime`, a `NonZero*`, a validating `deserialize_with`) go
1866 // undetected: the tag would be valid, the decode would still fail, and
1867 // this test would stay green while the counts drifted.
1868 //
1869 // bincode hands an out-of-range variant index to serde's derived
1870 // visitor, which rejects it as `invalid value: integer `N`, expected
1871 // variant index 0 <= i < M` — an `ErrorKind::Custom`. Match on that
1872 // wording rather than on `InvalidTagEncoding`, which bincode produces
1873 // only for a bad `Option` discriminant.
1874 fn assert_rejected_as_unknown_variant(err: &bincode::Error, tag: u32, which: &str) {
1875 let msg = err.to_string();
1876 assert!(
1877 msg.contains("variant index"),
1878 "tag {tag} on {which} failed for the wrong reason ({msg}); the tag itself must \
1879 still be unknown, otherwise a variant was added without updating the count, \
1880 the pinned_*_tag match and the every_* list"
1881 );
1882 }
1883
1884 let mut probe = INBOUND_VARIANT_COUNT.to_le_bytes().to_vec();
1885 probe.extend_from_slice(&[0u8; 256]);
1886 let err = match bincode::deserialize::<InboundDelegateMsg<'_>>(&probe) {
1887 Ok(v) => panic!(
1888 "tag {INBOUND_VARIANT_COUNT} must not decode as an InboundDelegateMsg, got {v:?}"
1889 ),
1890 Err(e) => e,
1891 };
1892 assert_rejected_as_unknown_variant(&err, INBOUND_VARIANT_COUNT, "InboundDelegateMsg");
1893
1894 let mut probe = OUTBOUND_VARIANT_COUNT.to_le_bytes().to_vec();
1895 probe.extend_from_slice(&[0u8; 256]);
1896 let err = match bincode::deserialize::<OutboundDelegateMsg>(&probe) {
1897 Ok(v) => panic!(
1898 "tag {OUTBOUND_VARIANT_COUNT} must not decode as an OutboundDelegateMsg, got {v:?}"
1899 ),
1900 Err(e) => e,
1901 };
1902 assert_rejected_as_unknown_variant(&err, OUTBOUND_VARIANT_COUNT, "OutboundDelegateMsg");
1903
1904 // Control, so the probe cannot pass vacuously from the other end: the
1905 // LAST known tag must still decode from the same all-zero payload. If
1906 // this ever fails, the zero payload has stopped being a valid encoding
1907 // for the final variant, and the probes above are no longer testing
1908 // what they claim.
1909 let mut control = (INBOUND_VARIANT_COUNT - 1).to_le_bytes().to_vec();
1910 control.extend_from_slice(&[0u8; 256]);
1911 bincode::deserialize::<InboundDelegateMsg<'_>>(&control).expect(
1912 "the LAST inbound variant's payload must be decodable from zeros, or this probe can \
1913 no longer tell an unknown tag from an unparseable payload. If a variant whose \
1914 payload rejects zeros was just appended, do not delete this — point the control at \
1915 a variant that still decodes from zeros",
1916 );
1917
1918 let mut control = (OUTBOUND_VARIANT_COUNT - 1).to_le_bytes().to_vec();
1919 control.extend_from_slice(&[0u8; 256]);
1920 bincode::deserialize::<OutboundDelegateMsg>(&control).expect(
1921 "the LAST outbound variant's payload must be decodable from zeros — see the inbound \
1922 control above for what to do if that stops being true",
1923 );
1924 }
1925
1926 /// Direction 1 of the append rule: **old sender to new receiver works.**
1927 ///
1928 /// The payload is hand-built rather than produced by this crate's own
1929 /// encoder, so it stands in for bytes emitted by a delegate compiled
1930 /// against an older stdlib; an encoder-produced value would only prove the
1931 /// code agrees with itself.
1932 ///
1933 /// Named for what it actually pins. Nothing here appends a variant — the
1934 /// test cannot fail *because of* an append, only because a tag moved or a
1935 /// payload layout changed, which `delegate_msg_variant_tags_are_pinned`
1936 /// also covers. Its distinct value is that the expected bytes are written
1937 /// out by hand, so a change to `ContractNotification`'s field order or to
1938 /// the bincode config fails here with a concrete byte string to compare
1939 /// against. Direction 2, which genuinely models an old receiver, is
1940 /// `a_new_variant_does_not_decode_on_an_old_receiver` below.
1941 #[test]
1942 fn a_hand_built_old_encoder_payload_decodes_into_the_same_variant() {
1943 // InboundDelegateMsg tag 6 = ContractNotification { contract_id,
1944 // new_state: WrappedState (empty), context: DelegateContext (empty) }.
1945 let mut old_payload = vec![6u8, 0, 0, 0];
1946 old_payload.extend_from_slice(&[0x5Au8; 32]);
1947 old_payload.extend_from_slice(&0u64.to_le_bytes()); // new_state: len 0
1948 old_payload.extend_from_slice(&0u64.to_le_bytes()); // context: len 0
1949
1950 let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&old_payload)
1951 .expect("a payload predating any appended variant must still decode");
1952 match decoded {
1953 InboundDelegateMsg::ContractNotification(n) => {
1954 assert_eq!(n.contract_id, instance_id());
1955 }
1956 other => panic!("an old ContractNotification decoded as {other:?}"),
1957 }
1958 }
1959
1960 /// Direction 2 of the append rule: **new sender to old receiver fails, and
1961 /// fails loudly.** This is the direction the docs warn about, so it is
1962 /// asserted rather than assumed.
1963 ///
1964 /// An old receiver is modelled by an enum with a truncated tag space,
1965 /// which is exactly what an older stdlib's version of these types is. The
1966 /// point is that the failure is an `Err` — not a silent mis-decode into
1967 /// whatever variant happens to sit at that index.
1968 #[test]
1969 fn a_new_variant_does_not_decode_on_an_old_receiver() {
1970 // An "old" OutboundDelegateMsg that knows tags 0..=6 only, i.e. one
1971 // built before `SendDelegateMessage` was appended at 7.
1972 // Variants are only ever produced by deserialization, never
1973 // constructed here — which is the whole point of the test.
1974 #[allow(dead_code)]
1975 #[derive(serde::Deserialize, Debug)]
1976 enum OldOutboundTagSpace {
1977 V0,
1978 V1,
1979 V2,
1980 V3,
1981 V4,
1982 V5,
1983 V6,
1984 }
1985
1986 let new_msg = bincode::serialize(&OutboundDelegateMsg::SendDelegateMessage(
1987 DelegateMessage::new(delegate_key(), delegate_key(), vec![0xEE]),
1988 ))
1989 .expect("outbound must serialize");
1990 assert_eq!(wire_tag(&new_msg), 7);
1991
1992 let decoded = bincode::deserialize::<OldOutboundTagSpace>(&new_msg);
1993 assert!(
1994 decoded.is_err(),
1995 "a receiver that predates a variant must REJECT it, not mis-decode it; \
1996 if this ever passes, the compatibility rule documented on \
1997 OutboundDelegateMsg is wrong and delegates are silently misreading messages"
1998 );
1999 }
2000
2001 /// The unsubscribe pair added in 0.10.0 round-trips, and adding it did not
2002 /// disturb any payload that predates it.
2003 ///
2004 /// The pre-0.10.0 byte string is hand-built rather than produced by this
2005 /// crate, so it stands in for bytes from a delegate compiled before the
2006 /// pair existed. Both halves matter: the new variant must work, and the old
2007 /// ones must be untouched by its arrival.
2008 #[test]
2009 fn the_unsubscribe_pair_round_trips_and_disturbs_nothing_older() {
2010 let id = instance_id();
2011
2012 let req =
2013 OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest::new(id));
2014 let encoded = bincode::serialize(&req).expect("request must serialize");
2015 assert_eq!(wire_tag(&encoded), 8, "unsubscribe request is frozen at 8");
2016 match bincode::deserialize::<OutboundDelegateMsg>(&encoded).expect("must round-trip") {
2017 OutboundDelegateMsg::UnsubscribeContractRequest(r) => {
2018 assert_eq!(r.contract_id, id);
2019 assert!(!r.processed);
2020 }
2021 other => panic!("round-tripped into {other:?}"),
2022 }
2023
2024 let resp = InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
2025 contract_id: id,
2026 result: Ok(()),
2027 context: DelegateContext::default(),
2028 });
2029 let encoded = bincode::serialize(&resp).expect("response must serialize");
2030 assert_eq!(wire_tag(&encoded), 8, "unsubscribe response is frozen at 8");
2031 match bincode::deserialize::<InboundDelegateMsg<'_>>(&encoded).expect("must round-trip") {
2032 InboundDelegateMsg::UnsubscribeContractResponse(r) => {
2033 // Assert the VALUES, not merely the variant. Checking only
2034 // `matches!` is what lets a field reorder through: the encoder
2035 // and decoder would still agree with each other.
2036 assert_eq!(r.contract_id, id);
2037 assert!(r.result.is_ok());
2038 }
2039 other => panic!("round-tripped into {other:?}"),
2040 }
2041
2042 // Both structs' doc comments say the field ORDER is the wire format.
2043 // A round-trip through this crate's own encoder cannot establish that —
2044 // it proves the code agrees with itself, and a swap of `contract_id`
2045 // and `result` would round-trip just as happily. So the layout is
2046 // frozen as hand-written bytes, the same way ContractNotification is.
2047 let mut expected_resp = vec![8u8, 0, 0, 0];
2048 expected_resp.extend_from_slice(&[0x5Au8; 32]); // contract_id
2049 expected_resp.extend_from_slice(&0u32.to_le_bytes()); // result: Ok variant tag
2050 expected_resp.extend_from_slice(&0u64.to_le_bytes()); // context: empty
2051 assert_eq!(
2052 encoded, expected_resp,
2053 "UnsubscribeContractResponse layout is frozen: tag, contract_id, result, context"
2054 );
2055
2056 let expected_req = {
2057 let mut v = vec![8u8, 0, 0, 0];
2058 v.extend_from_slice(&[0x5Au8; 32]); // contract_id
2059 v.extend_from_slice(&0u64.to_le_bytes()); // context: empty
2060 v.push(0u8); // processed: false
2061 v
2062 };
2063 assert_eq!(
2064 bincode::serialize(&req).expect("request must serialize"),
2065 expected_req,
2066 "UnsubscribeContractRequest layout is frozen: tag, contract_id, context, processed"
2067 );
2068
2069 // The error path has a different bincode shape from Ok and is part of
2070 // the same frozen layout, so it is exercised rather than assumed.
2071 let err_resp =
2072 InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
2073 contract_id: id,
2074 result: Err("nope".to_string()),
2075 context: DelegateContext::default(),
2076 });
2077 match bincode::deserialize::<InboundDelegateMsg<'_>>(
2078 &bincode::serialize(&err_resp).expect("must serialize"),
2079 )
2080 .expect("must round-trip")
2081 {
2082 InboundDelegateMsg::UnsubscribeContractResponse(r) => {
2083 assert_eq!(r.result.unwrap_err(), "nope");
2084 }
2085 other => panic!("error response round-tripped into {other:?}"),
2086 }
2087
2088 // A ContractNotification encoded before 0.10.0 existed: tag 6, the 32
2089 // raw id bytes, an empty state and an empty context. Appending at 8
2090 // must leave it decoding exactly as it always did.
2091 let mut pre_0_9_0 = vec![6u8, 0, 0, 0];
2092 pre_0_9_0.extend_from_slice(&[0x5Au8; 32]);
2093 pre_0_9_0.extend_from_slice(&0u64.to_le_bytes());
2094 pre_0_9_0.extend_from_slice(&0u64.to_le_bytes());
2095 match bincode::deserialize::<InboundDelegateMsg<'_>>(&pre_0_9_0)
2096 .expect("a pre-0.10.0 payload must still decode")
2097 {
2098 InboundDelegateMsg::ContractNotification(n) => assert_eq!(n.contract_id, id),
2099 other => panic!("a pre-0.10.0 ContractNotification decoded as {other:?}"),
2100 }
2101 }
2102
2103 /// Every inbound variant whose payload carries a `context` must return it.
2104 ///
2105 /// Both `get_context` and `get_mut_context` end in `_ => None`, so a
2106 /// missing arm is not a compile error — it silently reports "no context".
2107 /// That wildcard had already swallowed one: `UserResponse` carries a
2108 /// context and returned `None` for it, undetected, because nothing in the
2109 /// crate called either accessor.
2110 ///
2111 /// Driven off `every_inbound`, so a newly appended variant is covered the
2112 /// moment it is added to that list — which the tag pin already forces.
2113 #[test]
2114 fn every_inbound_variant_with_a_context_exposes_it() {
2115 for mut msg in every_inbound() {
2116 let tag = pinned_inbound_tag(&msg);
2117
2118 // `WakeupFired` is the one inbound variant with no context field,
2119 // and it is named here rather than skipped by a wildcard, matching
2120 // the outbound test below. See `get_context` for why it has none:
2121 // a context is per-conversation working state handed back on a
2122 // reply, and a wakeup opens a conversation rather than continuing
2123 // one. Carrying one would commit the host to persisting delegate
2124 // context across arbitrary wall-clock time, which is #5467 Phase 3.
2125 //
2126 // This asserts the accessor returns `None`, not that the struct
2127 // lacks a field. That distinction is the point: the claim "every
2128 // variant carries a context" was already false of this accessor in
2129 // 0.8.5, where it omitted `UserResponse` behind a `_ => None`
2130 // wildcard. Pin the behaviour, not the shape.
2131 if matches!(
2132 msg,
2133 InboundDelegateMsg::WakeupFired { .. } | InboundDelegateMsg::Lifecycle(_)
2134 ) {
2135 assert!(
2136 msg.get_context().is_none() && msg.get_mut_context().is_none(),
2137 "WakeupFired and Lifecycle are documented as carrying no context; if one grew one, remove this exemption rather than widening it"
2138 );
2139 continue;
2140 }
2141
2142 assert!(
2143 msg.get_context().is_some(),
2144 "InboundDelegateMsg tag {tag} has a context field but get_context returned None; \
2145 the `_ => None` wildcard hides a missing arm"
2146 );
2147 assert!(
2148 msg.get_mut_context().is_some(),
2149 "InboundDelegateMsg tag {tag} has a context field but get_mut_context returned \
2150 None; the two accessors must agree"
2151 );
2152 }
2153 }
2154
2155 /// The same, for the outbound side.
2156 ///
2157 /// `RequestUserInput` and `ContextUpdated` genuinely have no context field
2158 /// to return, so they are the two exceptions and are named explicitly
2159 /// rather than skipped by a wildcard.
2160 #[test]
2161 fn every_outbound_variant_with_a_context_exposes_it() {
2162 for mut msg in every_outbound() {
2163 let tag = pinned_outbound_tag(&msg);
2164 let has_no_context = matches!(
2165 msg,
2166 OutboundDelegateMsg::RequestUserInput(_) | OutboundDelegateMsg::ContextUpdated(_)
2167 );
2168 if has_no_context {
2169 continue;
2170 }
2171 assert!(
2172 msg.get_context().is_some(),
2173 "OutboundDelegateMsg tag {tag} has a context field but get_context returned None"
2174 );
2175 assert!(
2176 msg.get_mut_context().is_some(),
2177 "OutboundDelegateMsg tag {tag} has a context field but get_mut_context returned \
2178 None; the two accessors must agree"
2179 );
2180 }
2181 }
2182
2183 // ---------------------------------------------------------------------
2184 // `#[serde(other)]` — the one rule in WIRE-FORMAT.md that contradicts the
2185 // common advice, so it is the one a future reader will doubt and re-derive.
2186 // These three tests are that derivation, kept where it cannot rot.
2187 //
2188 // Mock types, deliberately: the real enums must never grow a catch-all, so
2189 // the property has to be demonstrated on stand-ins.
2190 // ---------------------------------------------------------------------
2191
2192 // The appended variants sit at tag 2, and `OldMsgWithCatchAll` declares
2193 // only 0 and 1. That gap is load-bearing: at tag 1 the catch-all's own
2194 // declared index, a plain unit variant decodes identically and the
2195 // attribute does no work at all — so mocks aligned that way pass with
2196 // `#[serde(other)]` deleted, testing nothing. Verified: they did.
2197 //
2198 // Both cases occur on a real append. The FIRST new variant lands exactly at
2199 // the catch-all's index, where the attribute is unnecessary; the SECOND is
2200 // out of range, where it is the only thing between a hard error and silent
2201 // corruption. The out-of-range case is the one the rule depends on, so it
2202 // is the one the mocks must produce.
2203 #[derive(Serialize, Deserialize, Debug, PartialEq)]
2204 enum NewMsgWithPayload {
2205 First(u32),
2206 Second(bool),
2207 Appended(String),
2208 }
2209
2210 #[derive(Serialize, Deserialize, Debug, PartialEq)]
2211 enum OldMsgWithCatchAll {
2212 First(u32),
2213 // Deliberately stops here: real variants 0 only, catch-all at 1. The
2214 // appended variants above are at tag 2, which is OUT OF RANGE for this
2215 // enum — that gap is what the attribute has to bridge.
2216 #[serde(other)]
2217 Unknown,
2218 }
2219
2220 #[derive(Serialize, Deserialize, Debug, PartialEq)]
2221 enum NewMsgUnitAppended {
2222 First(u32),
2223 Second(bool),
2224 AppendedUnit,
2225 }
2226
2227 /// The mocks' tag gap is asserted, not merely commented — and asserted
2228 /// against **the two enums whose alignment actually matters**.
2229 ///
2230 /// The vacuity condition is precisely: the tag `Appended` encodes to is the
2231 /// same as the index `OldMsgWithCatchAll` absorbs into `Unknown`. At that
2232 /// index a plain unit variant behaves identically and `#[serde(other)]`
2233 /// does no work, so the three tests below stop testing the attribute while
2234 /// still passing.
2235 ///
2236 /// Both numbers are measured from the types rather than written down, so
2237 /// this fires whichever side moves — adding a variant to the old enum, or
2238 /// removing the filler from the new ones. An earlier version of this guard
2239 /// compared against a separate no-attribute copy of the old enum and
2240 /// **missed the first case entirely**, because that copy did not move when
2241 /// the real one did. A control that can drift from what it controls is not
2242 /// a control.
2243 ///
2244 /// This exists because the alignment has broken **three times** in this
2245 /// file, twice at the hands of someone actively fixing it. A comment cannot
2246 /// catch the fourth.
2247 #[test]
2248 fn the_attribute_is_what_bridges_the_gap() {
2249 fn tag_of(bytes: &[u8]) -> u32 {
2250 u32::from_le_bytes(
2251 bytes[..4]
2252 .try_into()
2253 .expect("a bincode enum tag is 4 bytes"),
2254 )
2255 }
2256
2257 let appended =
2258 tag_of(&bincode::serialize(&NewMsgWithPayload::Appended("x".into())).unwrap());
2259
2260 // The lowest tag `OldMsgWithCatchAll` absorbs into `Unknown` is its
2261 // catch-all index; below it, real variants decode as themselves.
2262 let absorbed_from = (0u32..16)
2263 .find(|t| {
2264 let mut probe = t.to_le_bytes().to_vec();
2265 probe.extend_from_slice(&[0u8; 32]);
2266 matches!(
2267 bincode::deserialize::<OldMsgWithCatchAll>(&probe),
2268 Ok(OldMsgWithCatchAll::Unknown)
2269 )
2270 })
2271 .expect("OldMsgWithCatchAll must absorb some tag; it has #[serde(other)]");
2272
2273 assert!(
2274 appended > absorbed_from,
2275 "`Appended` is at tag {appended} and OldMsgWithCatchAll absorbs from tag \
2276 {absorbed_from}: the mocks have re-aligned, so the serde(other) tests below \
2277 are vacuous and pass with the attribute deleted. Move `Appended` above the \
2278 catch-all index again rather than adjusting this test."
2279 );
2280 }
2281
2282 /// Contradicts the usual "self-describing formats only" claim: bincode 1.x
2283 /// **does** let `#[serde(other)]` absorb an unknown variant tag.
2284 ///
2285 /// That is the trap, not a feature — see the next test for why.
2286 #[test]
2287 fn serde_other_does_absorb_an_unknown_tag_in_bincode() {
2288 let encoded = bincode::serialize(&NewMsgWithPayload::Appended("x".into())).unwrap();
2289 let decoded: OldMsgWithCatchAll =
2290 bincode::deserialize(&encoded).expect("serde(other) absorbs the unknown tag");
2291 assert_eq!(decoded, OldMsgWithCatchAll::Unknown);
2292 }
2293
2294 /// The absorption consumes the **tag only**, never the unknown variant's
2295 /// payload, so everything after it in the buffer is silently misread.
2296 ///
2297 /// A hard decode error would have been strictly better: this turns a loud,
2298 /// immediate failure into a wrong value with no error anywhere.
2299 #[test]
2300 fn the_catch_all_silently_corrupts_trailing_data() {
2301 let encoded =
2302 bincode::serialize(&(NewMsgWithPayload::Appended("hello-future".into()), 4242u32))
2303 .unwrap();
2304
2305 let (variant, trailing): (OldMsgWithCatchAll, u32) =
2306 bincode::deserialize(&encoded).expect("decodes, which is the problem");
2307
2308 assert_eq!(variant, OldMsgWithCatchAll::Unknown);
2309 assert_ne!(
2310 trailing, 4242,
2311 "if this ever equals 4242, serde(other) stopped eating the payload \
2312 and this section of WIRE-FORMAT.md needs revisiting"
2313 );
2314 }
2315
2316 /// And the reason the trap works: against a **unit** unknown variant there
2317 /// is no payload to leave behind, nothing after it is misread, and the
2318 /// decode really is clean.
2319 ///
2320 /// So a developer who tries `#[serde(other)]` on a unit variant sees it
2321 /// work and concludes the warning is overstated. The corruption is
2322 /// conditional on a property of a variant that does not exist yet — you are
2323 /// betting nobody ever gives a future variant a field.
2324 #[test]
2325 fn the_catch_all_is_clean_for_a_unit_variant() {
2326 let encoded = bincode::serialize(&(NewMsgUnitAppended::AppendedUnit, 4242u32)).unwrap();
2327
2328 let (variant, trailing): (OldMsgWithCatchAll, u32) =
2329 bincode::deserialize(&encoded).expect("unit variant leaves nothing behind");
2330
2331 assert_eq!(variant, OldMsgWithCatchAll::Unknown);
2332 assert_eq!(
2333 trailing, 4242,
2334 "a unit unknown variant must NOT corrupt what follows — this is the \
2335 case that misleads, and it is why the rule is unconditional"
2336 );
2337 }
2338}