Skip to main content

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 wakeup previously requested via
571    /// `DelegateCtx::schedule_wakeup` fires. `tag` is the opaque
572    /// value the delegate supplied when scheduling, echoed back verbatim so
573    /// the delegate can identify which wakeup fired. Owned (`'static`).
574    ///
575    /// # Currently unreachable, and deliberately kept — do not delete it
576    ///
577    /// This is the *delivery* half of scheduled wakeup. Its *request* half,
578    /// `DelegateCtx::schedule_wakeup`, was removed in 0.11.0 because no
579    /// released freenet-core ever registered the
580    /// `__frnt__delegate__schedule_wakeup` host import it called. Nothing can
581    /// ask for a wakeup today, so this variant never arrives.
582    ///
583    /// That makes it an orphan, and an orphan invites tidying. Three reasons
584    /// not to:
585    ///
586    /// - **Unreachable is not harmful.** The removed externs were removed
587    ///   because a delegate calling one compiles and then fails at module
588    ///   instantiation, leaving a healthy-looking node running a broken app.
589    ///   A variant that never arrives does none of that. Only the first
590    ///   problem justifies a breaking change.
591    /// - **This one is on the wire.** Deleting it is a wire-format change on a
592    ///   pinned enum, which is a much heavier act than deleting an unused
593    ///   `extern` declaration — and the tag-pinning test below exists to stop
594    ///   it happening casually.
595    /// - **The feature is expected back.** freenet-core's host-side
596    ///   implementation exists on the unmerged branch
597    ///   `feat/3972-delegate-wakeup-core`. Removing the delivery half now buys
598    ///   nothing and costs a second wire change when it lands.
599    ///
600    /// Restoring the feature means landing **both halves together**: the host
601    /// registration in freenet-core and the stdlib extern plus its
602    /// `host_imports::DECLARED_HOST_IMPORTS` entry. See freenet-core#5717 for
603    /// the check that makes that ordering visible.
604    ///
605    /// # What the context cache holds during a wakeup
606    ///
607    /// Nothing the delegate should read. freenet-core's delegate context cache
608    /// is keyed **per delegate**, not per conversation, and entries are pruned
609    /// after `DELEGATE_CONTEXT_TTL` (10 minutes). Two consequences, both
610    /// arguing the same way:
611    ///
612    /// - Any wakeup worth scheduling is far longer than 10 minutes, so whatever
613    ///   context existed when it was scheduled is **gone** by the time it fires.
614    /// - If the delegate happens to have a live context from some *other*
615    ///   in-flight exchange inside that window, it belongs to that exchange.
616    ///   Reading it during a wakeup would be reading another conversation's
617    ///   working state.
618    ///
619    /// This is why the variant carries no `DelegateContext`: there is no
620    /// coherent value to put in it. A delegate needing state across a wakeup
621    /// reads it from its secrets, which is what core's own cache doc
622    /// recommends for exactly this case.
623    ///
624    /// Appended at tag **9**, after `UnsubscribeContractResponse` at tag 8.
625    WakeupFired {
626        tag: Vec<u8>,
627    },
628    /// A lifecycle event: the delegate was installed on this node, or the
629    /// node started. See [`LifecycleEvent`](crate::prelude::LifecycleEvent).
630    ///
631    /// # What keeps this safe for deployed delegates
632    ///
633    /// A delegate built against an older stdlib cannot decode this tag (see
634    /// the wire-format note on this enum). The host sends it **only** to a
635    /// delegate whose embedded [`DelegateManifest`](crate::prelude::DelegateManifest)
636    /// lists the event's [`LifecycleKind`](crate::prelude::LifecycleKind), and
637    /// the manifest macro only lists kinds the delegate's own stdlib defines
638    /// (it names each one through the stdlib, so anything else fails to
639    /// compile). The first half is a property of the host implementation, not
640    /// of the format; freenet-core pins it.
641    ///
642    /// Carries no `DelegateContext`, for the same reason as `WakeupFired`: it
643    /// opens a conversation rather than continuing one.
644    ///
645    /// A client cannot send this variant: the host delivers it itself and
646    /// refuses it in a client's `ApplicationMessages`.
647    ///
648    /// Appended at tag **10**, after `WakeupFired` at tag 9.
649    Lifecycle(crate::delegate_manifest::LifecycleEvent),
650}
651
652impl InboundDelegateMsg<'_> {
653    pub fn into_owned(self) -> InboundDelegateMsg<'static> {
654        match self {
655            InboundDelegateMsg::ApplicationMessage(r) => InboundDelegateMsg::ApplicationMessage(r),
656            InboundDelegateMsg::UserResponse(r) => InboundDelegateMsg::UserResponse(r.into_owned()),
657            InboundDelegateMsg::GetContractResponse(r) => {
658                InboundDelegateMsg::GetContractResponse(r)
659            }
660            InboundDelegateMsg::PutContractResponse(r) => {
661                InboundDelegateMsg::PutContractResponse(r)
662            }
663            InboundDelegateMsg::UpdateContractResponse(r) => {
664                InboundDelegateMsg::UpdateContractResponse(r)
665            }
666            InboundDelegateMsg::SubscribeContractResponse(r) => {
667                InboundDelegateMsg::SubscribeContractResponse(r)
668            }
669            InboundDelegateMsg::ContractNotification(r) => {
670                InboundDelegateMsg::ContractNotification(r)
671            }
672            InboundDelegateMsg::DelegateMessage(r) => InboundDelegateMsg::DelegateMessage(r),
673            InboundDelegateMsg::UnsubscribeContractResponse(r) => {
674                InboundDelegateMsg::UnsubscribeContractResponse(r)
675            }
676            InboundDelegateMsg::WakeupFired { tag } => InboundDelegateMsg::WakeupFired { tag },
677            InboundDelegateMsg::Lifecycle(e) => InboundDelegateMsg::Lifecycle(e),
678        }
679    }
680
681    pub fn get_context(&self) -> Option<&DelegateContext> {
682        match self {
683            InboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
684                Some(context)
685            }
686            // UserResponse carries a context too. It was missing from both
687            // accessors, so this returned None for it — the `_ => None`
688            // wildcard below swallowed the omission silently. Found in review.
689            InboundDelegateMsg::UserResponse(UserInputResponse { context, .. }) => Some(context),
690            InboundDelegateMsg::GetContractResponse(GetContractResponse { context, .. }) => {
691                Some(context)
692            }
693            InboundDelegateMsg::PutContractResponse(PutContractResponse { context, .. }) => {
694                Some(context)
695            }
696            InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
697                context, ..
698            }) => Some(context),
699            InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
700                context,
701                ..
702            }) => Some(context),
703            InboundDelegateMsg::ContractNotification(ContractNotification { context, .. }) => {
704                Some(context)
705            }
706            InboundDelegateMsg::DelegateMessage(DelegateMessage { context, .. }) => Some(context),
707            InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
708                context,
709                ..
710            }) => Some(context),
711            // `WakeupFired` carries no `DelegateContext`, so `None` here is
712            // the honest answer rather than a missing arm. The reasoning lives
713            // on the variant itself -- see `InboundDelegateMsg::WakeupFired`,
714            // which explains both why a wakeup is not a reply and why the
715            // context cache could not supply a coherent value anyway. Kept in
716            // one place deliberately: a maintainer editing this accessor should
717            // not meet a second, older version of the argument.
718            InboundDelegateMsg::WakeupFired { .. } => None,
719            // `Lifecycle` carries no context either; see the variant.
720            InboundDelegateMsg::Lifecycle(_) => None,
721            // No wildcard, deliberately. The `_ => None` that used to sit here
722            // is what let UserResponse go unhandled and silently report "no
723            // context". Exhaustive means a new variant is a compile error here
724            // instead — which is how `WakeupFired` above came to be considered
725            // explicitly rather than defaulting into the wildcard.
726            //
727            // Correcting a premise this crate briefly asserted: "every variant
728            // carries a context" was already false before `WakeupFired`, and
729            // false about *this accessor* rather than about the structs. In
730            // 0.8.5 this match listed seven variants, omitted `UserResponse`
731            // — which does have a context field — and ended in `_ => None`. So
732            // the claim was true of the types and wrong about the code. That
733            // is why the `WakeupFired` exemption in the test asserts
734            // `get_context()` is `None`: it pins what this function does, not
735            // what the struct definitions look like.
736        }
737    }
738
739    pub fn get_mut_context(&mut self) -> Option<&mut DelegateContext> {
740        match self {
741            InboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
742                Some(context)
743            }
744            // UserResponse carries a context too. It was missing from both
745            // accessors, so this returned None for it — the `_ => None`
746            // wildcard below swallowed the omission silently. Found in review.
747            InboundDelegateMsg::UserResponse(UserInputResponse { context, .. }) => Some(context),
748            InboundDelegateMsg::GetContractResponse(GetContractResponse { context, .. }) => {
749                Some(context)
750            }
751            InboundDelegateMsg::PutContractResponse(PutContractResponse { context, .. }) => {
752                Some(context)
753            }
754            InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
755                context, ..
756            }) => Some(context),
757            InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
758                context,
759                ..
760            }) => Some(context),
761            InboundDelegateMsg::ContractNotification(ContractNotification { context, .. }) => {
762                Some(context)
763            }
764            InboundDelegateMsg::DelegateMessage(DelegateMessage { context, .. }) => Some(context),
765            InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
766                context,
767                ..
768            }) => Some(context),
769            // `WakeupFired` and `Lifecycle` carry no context; see `get_context`.
770            InboundDelegateMsg::WakeupFired { .. } => None,
771            InboundDelegateMsg::Lifecycle(_) => None,
772            // No wildcard, deliberately. The `_ => None` that used to sit here
773            // is what let UserResponse go unhandled and silently report "no
774            // context". Exhaustive means a new variant is a compile error here
775            // instead.
776        }
777    }
778}
779
780impl From<ApplicationMessage> for InboundDelegateMsg<'_> {
781    fn from(value: ApplicationMessage) -> Self {
782        Self::ApplicationMessage(value)
783    }
784}
785
786impl<'a> TryFromFbs<&FbsInboundDelegateMsg<'a>> for InboundDelegateMsg<'a> {
787    fn try_decode_fbs(msg: &FbsInboundDelegateMsg<'a>) -> Result<Self, WsApiError> {
788        match msg.inbound_type() {
789            InboundDelegateMsgType::common_ApplicationMessage => {
790                let app_msg = msg.inbound_as_common_application_message().unwrap();
791                let app_msg = ApplicationMessage {
792                    payload: app_msg.payload().bytes().to_vec(),
793                    context: DelegateContext::new(app_msg.context().bytes().to_vec()),
794                    processed: app_msg.processed(),
795                };
796                Ok(InboundDelegateMsg::ApplicationMessage(app_msg))
797            }
798            InboundDelegateMsgType::UserInputResponse => {
799                let user_response = msg.inbound_as_user_input_response().unwrap();
800                let user_response = UserInputResponse {
801                    request_id: user_response.request_id(),
802                    response: ClientResponse::new(user_response.response().data().bytes().to_vec()),
803                    context: DelegateContext::new(
804                        user_response.delegate_context().bytes().to_vec(),
805                    ),
806                };
807                Ok(InboundDelegateMsg::UserResponse(user_response))
808            }
809            // Reachable, not `unreachable!()`: the generated verifier for this
810            // union ends in `_ => Ok(())`, so any discriminant a client sets —
811            // including `NONE` — arrives here. See `unknown_union_discriminant`.
812            other => Err(unknown_union_discriminant(
813                "InboundDelegateMsgType",
814                other.0,
815            )),
816        }
817    }
818}
819
820#[non_exhaustive]
821#[derive(Serialize, Deserialize, Debug, Clone)]
822pub struct ApplicationMessage {
823    pub payload: Vec<u8>,
824    pub context: DelegateContext,
825    pub processed: bool,
826}
827
828impl ApplicationMessage {
829    pub fn new(payload: Vec<u8>) -> Self {
830        Self {
831            payload,
832            context: DelegateContext::default(),
833            processed: false,
834        }
835    }
836
837    pub fn with_context(mut self, context: DelegateContext) -> Self {
838        self.context = context;
839        self
840    }
841
842    pub fn processed(mut self, p: bool) -> Self {
843        self.processed = p;
844        self
845    }
846}
847
848#[derive(Serialize, Deserialize, Debug, Clone)]
849pub struct UserInputResponse<'a> {
850    pub request_id: u32,
851    #[serde(borrow)]
852    pub response: ClientResponse<'a>,
853    pub context: DelegateContext,
854}
855
856impl UserInputResponse<'_> {
857    pub fn into_owned(self) -> UserInputResponse<'static> {
858        UserInputResponse {
859            request_id: self.request_id,
860            response: self.response.into_owned(),
861            context: self.context,
862        }
863    }
864}
865
866/// Messages emitted **out of** a delegate's `process()` function.
867///
868/// This is the outbound counterpart of [`InboundDelegateMsg`] and sits on the
869/// same host↔delegate wire boundary.
870///
871/// # Deliberately not `#[non_exhaustive]`
872///
873/// Adding a variant here is a source-level break for any downstream crate that
874/// matches on it exhaustively. That is the intended behaviour and it should not
875/// be "fixed" by marking the enum.
876///
877/// Every variant of this enum is a **request the host must act on**. There is
878/// one host — freenet-core — and it dispatches these in exhaustive matches with
879/// no wildcard (`crates/core/src/contract.rs`, in the request loop and again in
880/// the app-message filter). Marking this enum `#[non_exhaustive]` would force
881/// those matches to grow `_ =>` arms, and a newly added variant would then
882/// compile against the host with **no arm of its own**: the delegate's request
883/// would fall into the wildcard, the call would appear to succeed, and nothing
884/// would report that it did nothing.
885///
886/// The compile error is what stops that, and it is the only mechanism that
887/// does. Keep it.
888///
889/// Two honest limits on this argument, because it is easy to claim more:
890///
891/// - **It forces an arm to exist, not a handler to be correct.** This crate's
892///   own FlatBuffers encoder (`client_api::client_events`) has explicit arms
893///   for six outbound variants that log an error and drop the message. The
894///   compile error made someone write those arms deliberately; it could not
895///   make them do anything useful.
896/// - **It is not the bug behind this workstream.** A delegate
897///   `SubscribeContractRequest` *is* handled by the host today. Its defect is
898///   different and subtler: it registers no demand in the network, so the
899///   subscription does not pin the contract (freenet-core#4669). Do not read
900///   the compile-error argument as a fix for that; it is a guard against a
901///   different failure that has not happened yet, which is the point of a
902///   guard.
903///
904/// [`InboundDelegateMsg`] carries the opposite trade-off, and is marked: its
905/// consumers are third-party delegate WASM, which can reasonably ignore a
906/// variant it does not know about.
907///
908/// # Wire format and compatibility
909///
910/// bincode, variant index 0..=N in **declaration order**. Never insert or
911/// reorder a variant: that silently reassigns every later tag, and deployed
912/// delegate WASM built against an older stdlib would encode into what the host
913/// now reads as a different variant. `delegate_msg_variant_tags_are_pinned`
914/// pins every tag so a reorder fails CI rather than production.
915///
916/// Appending is compatible in one direction only, and this enum is the
917/// direction that bites:
918///
919/// - **Old delegate → new host: fine, for appended VARIANTS.** The host
920///   understands every tag an older delegate can emit, so deployed delegate
921///   WASM keeps working against an upgraded node with no rebuild. This does
922///   **not** extend to appending a FIELD to an existing variant's payload
923///   struct, because a field breaks in the opposite direction. See
924///   `struct_field_wire_compat` in `client_api::client_events`.
925///   (`ApplicationMessage` is `#[non_exhaustive]`, which invites precisely that
926///   edit. It is the only payload struct here that is.)
927/// - **New delegate → old host: fails, and fails loudly.** bincode rejects the
928///   unknown variant tag — as `ErrorKind::Custom("invalid value: integer `N`,
929///   expected variant index 0 <= i < M")`, since it hands the index to serde's
930///   derived visitor rather than validating it itself — so the host surfaces a
931///   decode error on that message rather than misreading it.
932///
933/// There is deliberately **no feature-detection handshake**. A delegate cannot
934/// ask the host which variants it understands, and adding a probe would itself
935/// be a wire change with the same bootstrapping problem. The rule is therefore
936/// the blunt one: **a delegate that emits a variant introduced in stdlib
937/// version X requires a host built against stdlib >= X.**
938///
939/// Where a host function exists for the same capability, it is the better
940/// choice against older hosts. Host functions are resolved **by name at module
941/// instantiation**, so an import an old host does not provide fails at load
942/// time with a named missing-import error, instead of mid-protocol on a decode.
943///
944/// That said, the `freenet_delegate_contracts` namespace holds only
945/// `get_contract_state(_len)` — a local read. There is no host function for
946/// writing or subscribing, so `PutContractRequest`, `UpdateContractRequest` and
947/// `SubscribeContractRequest` below are the only route for those, and the
948/// variant rule above governs them.
949#[derive(Serialize, Deserialize, Debug, Clone)]
950pub enum OutboundDelegateMsg {
951    // for the apps
952    ApplicationMessage(ApplicationMessage),
953    RequestUserInput(
954        #[serde(deserialize_with = "OutboundDelegateMsg::deser_user_input_req")]
955        UserInputRequest<'static>,
956    ),
957    // todo: remove when context can be accessed from the delegate environment and we pass it as reference
958    ContextUpdated(DelegateContext),
959    GetContractRequest(GetContractRequest),
960    PutContractRequest(PutContractRequest),
961    UpdateContractRequest(UpdateContractRequest),
962    SubscribeContractRequest(SubscribeContractRequest),
963    SendDelegateMessage(DelegateMessage),
964    // Appended in 0.10.0 at tag 8. New variants go at the END, never inserted —
965    // see the wire-format note on this enum.
966    UnsubscribeContractRequest(UnsubscribeContractRequest),
967}
968
969impl From<ApplicationMessage> for OutboundDelegateMsg {
970    fn from(req: ApplicationMessage) -> Self {
971        Self::ApplicationMessage(req)
972    }
973}
974
975impl From<GetContractRequest> for OutboundDelegateMsg {
976    fn from(req: GetContractRequest) -> Self {
977        Self::GetContractRequest(req)
978    }
979}
980
981impl From<PutContractRequest> for OutboundDelegateMsg {
982    fn from(req: PutContractRequest) -> Self {
983        Self::PutContractRequest(req)
984    }
985}
986
987impl From<UpdateContractRequest> for OutboundDelegateMsg {
988    fn from(req: UpdateContractRequest) -> Self {
989        Self::UpdateContractRequest(req)
990    }
991}
992
993impl From<SubscribeContractRequest> for OutboundDelegateMsg {
994    fn from(req: SubscribeContractRequest) -> Self {
995        Self::SubscribeContractRequest(req)
996    }
997}
998
999impl From<UnsubscribeContractRequest> for OutboundDelegateMsg {
1000    fn from(req: UnsubscribeContractRequest) -> Self {
1001        Self::UnsubscribeContractRequest(req)
1002    }
1003}
1004
1005impl From<DelegateMessage> for OutboundDelegateMsg {
1006    fn from(msg: DelegateMessage) -> Self {
1007        Self::SendDelegateMessage(msg)
1008    }
1009}
1010
1011impl OutboundDelegateMsg {
1012    fn deser_user_input_req<'de, D>(deser: D) -> Result<UserInputRequest<'static>, D::Error>
1013    where
1014        D: serde::Deserializer<'de>,
1015    {
1016        let value = <UserInputRequest<'de> as Deserialize>::deserialize(deser)?;
1017        Ok(value.into_owned())
1018    }
1019
1020    pub fn processed(&self) -> bool {
1021        match self {
1022            OutboundDelegateMsg::ApplicationMessage(msg) => msg.processed,
1023            OutboundDelegateMsg::GetContractRequest(msg) => msg.processed,
1024            OutboundDelegateMsg::PutContractRequest(msg) => msg.processed,
1025            OutboundDelegateMsg::UpdateContractRequest(msg) => msg.processed,
1026            OutboundDelegateMsg::SubscribeContractRequest(msg) => msg.processed,
1027            OutboundDelegateMsg::UnsubscribeContractRequest(msg) => msg.processed,
1028            OutboundDelegateMsg::SendDelegateMessage(msg) => msg.processed,
1029            OutboundDelegateMsg::RequestUserInput(_) => true,
1030            OutboundDelegateMsg::ContextUpdated(_) => true,
1031        }
1032    }
1033
1034    pub fn get_context(&self) -> Option<&DelegateContext> {
1035        match self {
1036            OutboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
1037                Some(context)
1038            }
1039            OutboundDelegateMsg::GetContractRequest(GetContractRequest { context, .. }) => {
1040                Some(context)
1041            }
1042            OutboundDelegateMsg::PutContractRequest(PutContractRequest { context, .. }) => {
1043                Some(context)
1044            }
1045            OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
1046                context, ..
1047            }) => Some(context),
1048            OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
1049                context,
1050                ..
1051            }) => Some(context),
1052            OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest {
1053                context,
1054                ..
1055            }) => Some(context),
1056            OutboundDelegateMsg::SendDelegateMessage(DelegateMessage { context, .. }) => {
1057                Some(context)
1058            }
1059            _ => None,
1060        }
1061    }
1062
1063    pub fn get_mut_context(&mut self) -> Option<&mut DelegateContext> {
1064        match self {
1065            OutboundDelegateMsg::ApplicationMessage(ApplicationMessage { context, .. }) => {
1066                Some(context)
1067            }
1068            OutboundDelegateMsg::GetContractRequest(GetContractRequest { context, .. }) => {
1069                Some(context)
1070            }
1071            OutboundDelegateMsg::PutContractRequest(PutContractRequest { context, .. }) => {
1072                Some(context)
1073            }
1074            OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest {
1075                context, ..
1076            }) => Some(context),
1077            OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest {
1078                context,
1079                ..
1080            }) => Some(context),
1081            OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest {
1082                context,
1083                ..
1084            }) => Some(context),
1085            OutboundDelegateMsg::SendDelegateMessage(DelegateMessage { context, .. }) => {
1086                Some(context)
1087            }
1088            _ => None,
1089        }
1090    }
1091}
1092
1093/// Request to get contract state from within a delegate.
1094#[derive(Serialize, Deserialize, Debug, Clone)]
1095pub struct GetContractRequest {
1096    pub contract_id: ContractInstanceId,
1097    pub context: DelegateContext,
1098    pub processed: bool,
1099}
1100
1101impl GetContractRequest {
1102    pub fn new(contract_id: ContractInstanceId) -> Self {
1103        Self {
1104            contract_id,
1105            context: Default::default(),
1106            processed: false,
1107        }
1108    }
1109}
1110
1111/// Response containing contract state for a delegate.
1112#[derive(Serialize, Deserialize, Debug, Clone)]
1113pub struct GetContractResponse {
1114    pub contract_id: ContractInstanceId,
1115    /// The contract state, or None if the contract was not found locally.
1116    pub state: Option<WrappedState>,
1117    pub context: DelegateContext,
1118}
1119
1120/// Request to store a new contract from within a delegate.
1121#[derive(Serialize, Deserialize, Debug, Clone)]
1122pub struct PutContractRequest {
1123    /// The contract code and parameters.
1124    pub contract: ContractContainer,
1125    /// The initial state for the contract.
1126    pub state: WrappedState,
1127    /// Related contracts that this contract depends on.
1128    #[serde(deserialize_with = "RelatedContracts::deser_related_contracts")]
1129    pub related_contracts: RelatedContracts<'static>,
1130    /// Context for the delegate.
1131    pub context: DelegateContext,
1132    /// Whether this request has been processed.
1133    pub processed: bool,
1134}
1135
1136impl PutContractRequest {
1137    pub fn new(
1138        contract: ContractContainer,
1139        state: WrappedState,
1140        related_contracts: RelatedContracts<'static>,
1141    ) -> Self {
1142        Self {
1143            contract,
1144            state,
1145            related_contracts,
1146            context: Default::default(),
1147            processed: false,
1148        }
1149    }
1150}
1151
1152/// Response after attempting to store a contract from a delegate.
1153#[derive(Serialize, Deserialize, Debug, Clone)]
1154pub struct PutContractResponse {
1155    /// The ID of the contract that was (attempted to be) stored.
1156    pub contract_id: ContractInstanceId,
1157    /// Success (Ok) or error message (Err).
1158    pub result: Result<(), String>,
1159    /// Context for the delegate.
1160    pub context: DelegateContext,
1161}
1162
1163/// Request to update an existing contract's state from within a delegate.
1164#[derive(Serialize, Deserialize, Debug, Clone)]
1165pub struct UpdateContractRequest {
1166    /// The contract to update.
1167    pub contract_id: ContractInstanceId,
1168    /// The update to apply (full state or delta).
1169    #[serde(deserialize_with = "UpdateContractRequest::deser_update_data")]
1170    pub update: UpdateData<'static>,
1171    /// Context for the delegate.
1172    pub context: DelegateContext,
1173    /// Whether this request has been processed.
1174    pub processed: bool,
1175}
1176
1177impl UpdateContractRequest {
1178    pub fn new(contract_id: ContractInstanceId, update: UpdateData<'static>) -> Self {
1179        Self {
1180            contract_id,
1181            update,
1182            context: Default::default(),
1183            processed: false,
1184        }
1185    }
1186
1187    fn deser_update_data<'de, D>(deser: D) -> Result<UpdateData<'static>, D::Error>
1188    where
1189        D: Deserializer<'de>,
1190    {
1191        let value = <UpdateData<'de> as Deserialize>::deserialize(deser)?;
1192        Ok(value.into_owned())
1193    }
1194}
1195
1196/// Response after attempting to update a contract from a delegate.
1197#[derive(Serialize, Deserialize, Debug, Clone)]
1198pub struct UpdateContractResponse {
1199    /// The contract that was updated.
1200    pub contract_id: ContractInstanceId,
1201    /// Success (Ok) or error message (Err).
1202    pub result: Result<(), String>,
1203    /// Context for the delegate.
1204    pub context: DelegateContext,
1205}
1206
1207/// Request to subscribe to a contract's state changes from within a delegate.
1208#[derive(Serialize, Deserialize, Debug, Clone)]
1209pub struct SubscribeContractRequest {
1210    /// The contract to subscribe to.
1211    pub contract_id: ContractInstanceId,
1212    /// Context for the delegate.
1213    pub context: DelegateContext,
1214    /// Whether this request has been processed.
1215    pub processed: bool,
1216}
1217
1218impl SubscribeContractRequest {
1219    pub fn new(contract_id: ContractInstanceId) -> Self {
1220        Self {
1221            contract_id,
1222            context: Default::default(),
1223            processed: false,
1224        }
1225    }
1226}
1227
1228/// Response after attempting to subscribe to a contract from a delegate.
1229#[derive(Serialize, Deserialize, Debug, Clone)]
1230pub struct SubscribeContractResponse {
1231    /// The contract subscribed to.
1232    pub contract_id: ContractInstanceId,
1233    /// Success (Ok) or error message (Err).
1234    pub result: Result<(), String>,
1235    /// Context for the delegate.
1236    pub context: DelegateContext,
1237}
1238
1239/// Request to stop receiving a contract's state changes, from within a delegate.
1240///
1241/// The counterpart of [`SubscribeContractRequest`]. Before 0.10.0 a delegate had
1242/// no way to drop a subscription it had taken: the only release path was the
1243/// implicit cleanup when the delegate itself was unregistered, so a delegate
1244/// that had finished with a contract went on holding interest in it for as long
1245/// as the delegate existed. Specified in freenet-core#2830 alongside subscribe;
1246/// only subscribe was built.
1247///
1248/// Answered with [`InboundDelegateMsg::UnsubscribeContractResponse`].
1249///
1250/// Field order is the wire format. Do not reorder.
1251#[derive(Serialize, Deserialize, Debug, Clone)]
1252pub struct UnsubscribeContractRequest {
1253    /// The contract to stop receiving notifications for.
1254    pub contract_id: ContractInstanceId,
1255    /// Context for the delegate.
1256    pub context: DelegateContext,
1257    /// Whether this request has been processed.
1258    pub processed: bool,
1259}
1260
1261impl UnsubscribeContractRequest {
1262    pub fn new(contract_id: ContractInstanceId) -> Self {
1263        Self {
1264            contract_id,
1265            context: Default::default(),
1266            processed: false,
1267        }
1268    }
1269}
1270
1271/// Response after attempting to unsubscribe from a contract from a delegate.
1272///
1273/// **Unsubscribing a contract the delegate is not subscribed to reports
1274/// `Ok(())`, not an error.** That is not a convenience: it is what the host
1275/// actually does. Teardown goes through the same removal path that a
1276/// no-longer-present client id already takes as a no-op, so returning an error
1277/// would have the host inventing a failure it did not have. It also matches the
1278/// subscribe side, where a repeat subscribe is a set insert.
1279///
1280/// Field order is the wire format. Do not reorder.
1281#[derive(Serialize, Deserialize, Debug, Clone)]
1282pub struct UnsubscribeContractResponse {
1283    /// The contract unsubscribed from.
1284    pub contract_id: ContractInstanceId,
1285    /// Success (Ok) or error message (Err). Unsubscribing a contract the
1286    /// delegate was not subscribed to reports `Ok(())`.
1287    pub result: Result<(), String>,
1288    /// Context for the delegate.
1289    pub context: DelegateContext,
1290}
1291
1292/// A message sent from one delegate to another.
1293///
1294/// Delegates can communicate with each other by emitting
1295/// `OutboundDelegateMsg::SendDelegateMessage` with a `DelegateMessage` targeting
1296/// another delegate. The runtime delivers it as `InboundDelegateMsg::DelegateMessage`
1297/// to the target delegate's `process()` function.
1298///
1299/// The `sender` field is overwritten by the runtime with the actual sender's key
1300/// (sender attestation), so delegates cannot spoof their identity.
1301#[derive(Serialize, Deserialize, Debug, Clone)]
1302pub struct DelegateMessage {
1303    /// The delegate to deliver this message to.
1304    pub target: DelegateKey,
1305    /// The delegate that sent this message (overwritten by runtime for attestation).
1306    pub sender: DelegateKey,
1307    /// Arbitrary message payload.
1308    pub payload: Vec<u8>,
1309    /// Delegate context, carried through the processing pipeline.
1310    pub context: DelegateContext,
1311    /// Runtime protocol flag indicating whether this message has been delivered.
1312    pub processed: bool,
1313}
1314
1315impl DelegateMessage {
1316    pub fn new(target: DelegateKey, sender: DelegateKey, payload: Vec<u8>) -> Self {
1317        Self {
1318            target,
1319            sender,
1320            payload,
1321            context: DelegateContext::default(),
1322            processed: false,
1323        }
1324    }
1325}
1326
1327/// Notification delivered to a delegate when a subscribed contract's state changes.
1328#[derive(Serialize, Deserialize, Debug, Clone)]
1329pub struct ContractNotification {
1330    /// The contract whose state changed.
1331    pub contract_id: ContractInstanceId,
1332    /// The new state of the contract.
1333    pub new_state: WrappedState,
1334    /// Context for the delegate.
1335    pub context: DelegateContext,
1336}
1337
1338#[serde_as]
1339#[derive(Serialize, Deserialize, Debug, Clone)]
1340pub struct NotificationMessage<'a>(
1341    #[serde_as(as = "serde_with::Bytes")]
1342    #[serde(borrow)]
1343    Cow<'a, [u8]>,
1344);
1345
1346impl TryFrom<&serde_json::Value> for NotificationMessage<'static> {
1347    type Error = ();
1348
1349    fn try_from(json: &serde_json::Value) -> Result<NotificationMessage<'static>, ()> {
1350        // todo: validate format when we have a better idea of what we want here
1351        let bytes = serde_json::to_vec(json).unwrap();
1352        Ok(Self(Cow::Owned(bytes)))
1353    }
1354}
1355
1356impl NotificationMessage<'_> {
1357    pub fn into_owned(self) -> NotificationMessage<'static> {
1358        NotificationMessage(self.0.into_owned().into())
1359    }
1360    pub fn bytes(&self) -> &[u8] {
1361        self.0.as_ref()
1362    }
1363}
1364
1365#[serde_as]
1366#[derive(Serialize, Deserialize, Debug, Clone)]
1367pub struct ClientResponse<'a>(
1368    #[serde_as(as = "serde_with::Bytes")]
1369    #[serde(borrow)]
1370    Cow<'a, [u8]>,
1371);
1372
1373impl Deref for ClientResponse<'_> {
1374    type Target = [u8];
1375
1376    fn deref(&self) -> &Self::Target {
1377        &self.0
1378    }
1379}
1380
1381impl ClientResponse<'_> {
1382    pub fn new(response: Vec<u8>) -> Self {
1383        Self(response.into())
1384    }
1385    pub fn into_owned(self) -> ClientResponse<'static> {
1386        ClientResponse(self.0.into_owned().into())
1387    }
1388    pub fn bytes(&self) -> &[u8] {
1389        self.0.as_ref()
1390    }
1391}
1392
1393#[derive(Serialize, Deserialize, Debug, Clone)]
1394pub struct UserInputRequest<'a> {
1395    pub request_id: u32,
1396    #[serde(borrow)]
1397    /// An interpretable message by the notification system.
1398    pub message: NotificationMessage<'a>,
1399    /// If a response is required from the user they can be chosen from this list.
1400    pub responses: Vec<ClientResponse<'a>>,
1401}
1402
1403impl UserInputRequest<'_> {
1404    pub fn into_owned(self) -> UserInputRequest<'static> {
1405        UserInputRequest {
1406            request_id: self.request_id,
1407            message: self.message.into_owned(),
1408            responses: self.responses.into_iter().map(|r| r.into_owned()).collect(),
1409        }
1410    }
1411}
1412
1413#[doc(hidden)]
1414pub(crate) mod wasm_interface {
1415    //! Contains all the types to interface between the host environment and
1416    //! the wasm module execution.
1417    use super::*;
1418    use crate::memory::WasmLinearMem;
1419
1420    #[repr(C)]
1421    #[derive(Debug, Clone, Copy)]
1422    pub struct DelegateInterfaceResult {
1423        ptr: i64,
1424        size: u32,
1425    }
1426
1427    impl DelegateInterfaceResult {
1428        pub unsafe fn from_raw(ptr: i64, mem: &WasmLinearMem) -> Self {
1429            let result = Box::leak(Box::from_raw(crate::memory::buf::compute_ptr(
1430                ptr as *mut Self,
1431                mem,
1432            )));
1433            #[cfg(feature = "trace")]
1434            {
1435                tracing::trace!(
1436                    "got FFI result @ {ptr} ({:p}) -> {result:?}",
1437                    ptr as *mut Self
1438                );
1439            }
1440            *result
1441        }
1442
1443        #[cfg(feature = "contract")]
1444        pub fn into_raw(self) -> i64 {
1445            #[cfg(feature = "trace")]
1446            {
1447                tracing::trace!("returning FFI -> {self:?}");
1448            }
1449            let ptr = Box::into_raw(Box::new(self));
1450            #[cfg(feature = "trace")]
1451            {
1452                tracing::trace!("FFI result ptr: {ptr:p} ({}i64)", ptr as i64);
1453            }
1454            ptr as _
1455        }
1456
1457        pub unsafe fn unwrap(
1458            self,
1459            mem: WasmLinearMem,
1460        ) -> Result<Vec<OutboundDelegateMsg>, DelegateError> {
1461            let ptr = crate::memory::buf::compute_ptr(self.ptr as *mut u8, &mem);
1462            let serialized = std::slice::from_raw_parts(ptr as *const u8, self.size as _);
1463            let value: Result<Vec<OutboundDelegateMsg>, DelegateError> =
1464                bincode::deserialize(serialized)
1465                    .map_err(|e| DelegateError::Other(format!("{e}")))?;
1466            #[cfg(feature = "trace")]
1467            {
1468                tracing::trace!(
1469                    "got result through FFI; addr: {:p} ({}i64, mapped: {ptr:p})
1470                     serialized: {serialized:?}
1471                     value: {value:?}",
1472                    self.ptr as *mut u8,
1473                    self.ptr
1474                );
1475            }
1476            value
1477        }
1478    }
1479
1480    impl From<Result<Vec<OutboundDelegateMsg>, DelegateError>> for DelegateInterfaceResult {
1481        fn from(value: Result<Vec<OutboundDelegateMsg>, DelegateError>) -> Self {
1482            let serialized = bincode::serialize(&value).unwrap();
1483            let size = serialized.len() as _;
1484            let ptr = serialized.as_ptr();
1485            #[cfg(feature = "trace")]
1486            {
1487                tracing::trace!(
1488                    "sending result through FFI; addr: {ptr:p} ({}),\n  serialized: {serialized:?}\n  value: {value:?}",
1489                    ptr as i64
1490                );
1491            }
1492            std::mem::forget(serialized);
1493            Self {
1494                ptr: ptr as i64,
1495                size,
1496            }
1497        }
1498    }
1499}
1500
1501#[cfg(test)]
1502mod message_origin_tests {
1503    use super::*;
1504
1505    /// Wire-format pin: bincode encoding of `MessageOrigin::WebApp(..)` must
1506    /// stay byte-identical across stdlib releases. Deployed delegate WASM
1507    /// compiled against an older stdlib will receive these bytes from a
1508    /// host running the new stdlib and must continue to deserialize them.
1509    /// If this test ever fails, it is a wire-format break and is NOT
1510    /// publishable as a non-major bump.
1511    #[test]
1512    fn webapp_origin_wire_format_is_stable() {
1513        let id = ContractInstanceId::new([0xABu8; 32]);
1514        let origin = MessageOrigin::WebApp(id);
1515        let encoded = bincode::serialize(&origin).unwrap();
1516
1517        // Variant tag 0 (4-byte LE u32 in default bincode config) followed by
1518        // the 32 raw bytes of the ContractInstanceId.
1519        let mut expected = vec![0u8, 0, 0, 0];
1520        expected.extend_from_slice(&[0xABu8; 32]);
1521        assert_eq!(encoded, expected);
1522    }
1523
1524    /// Wire-format pin for the `Delegate` variant. Locks the full byte
1525    /// layout (variant tag + serde repr of `DelegateKey`) so that any future
1526    /// change to either `DelegateKey`'s serde or the workspace bincode
1527    /// config is caught loudly. If `DelegateKey`'s on-the-wire encoding
1528    /// changes, deployed delegates compiled against a previous stdlib will
1529    /// silently fail to deserialize inter-delegate origins — which is
1530    /// exactly the failure mode this test exists to prevent.
1531    #[test]
1532    fn delegate_origin_wire_format_is_stable() {
1533        let key = DelegateKey::new([0x11u8; 32], crate::code_hash::CodeHash::new([0x22u8; 32]));
1534        let origin = MessageOrigin::Delegate(key);
1535        let encoded = bincode::serialize(&origin).unwrap();
1536
1537        // Variant tag 1 (4-byte LE u32 in default bincode config), followed
1538        // by the 32-byte `key` field, followed by the 32-byte `code_hash`
1539        // field of `DelegateKey`.
1540        let mut expected = vec![1u8, 0, 0, 0];
1541        expected.extend_from_slice(&[0x11u8; 32]);
1542        expected.extend_from_slice(&[0x22u8; 32]);
1543        assert_eq!(encoded, expected);
1544
1545        // And it must still round-trip.
1546        let decoded: MessageOrigin = bincode::deserialize(&encoded).unwrap();
1547        assert!(matches!(decoded, MessageOrigin::Delegate(_)));
1548    }
1549
1550    /// Wire-format pin for the first variant of [`InboundDelegateMsg`]. Pins
1551    /// the tag so that reordering the enum cannot silently shift existing
1552    /// deployed delegate WASM off the correct variant. Only tag+payload
1553    /// prefix is asserted (not the full ApplicationMessage byte layout),
1554    /// since ApplicationMessage's internal fields have their own stability
1555    /// expectations handled at a different layer. What matters here is that
1556    /// variant 0 stays `ApplicationMessage` on the wire.
1557    #[test]
1558    fn inbound_delegate_msg_wire_format_is_stable() {
1559        let msg = InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC]));
1560        let encoded = bincode::serialize(&msg).unwrap();
1561        assert_eq!(
1562            encoded[..4],
1563            [0, 0, 0, 0],
1564            "ApplicationMessage must stay at variant tag 0 on the wire; \
1565             reordering InboundDelegateMsg variants is a wire-format break"
1566        );
1567        // And it must still round-trip into the same variant.
1568        let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&encoded).unwrap();
1569        assert!(matches!(decoded, InboundDelegateMsg::ApplicationMessage(_)));
1570    }
1571
1572    /// Wire-format pin for [`InboundDelegateMsg::WakeupFired`]. It is the 10th
1573    /// variant (declaration index 9), so its bincode tag must be `9` (4-byte
1574    /// LE) — it sits behind `UnsubscribeContractResponse` at tag 8. Once
1575    /// shipped this tag is frozen: reordering or inserting a variant ahead of
1576    /// it would silently redirect a host's wakeup delivery to the wrong variant
1577    /// on a delegate compiled against this stdlib.
1578    #[test]
1579    fn inbound_wakeup_fired_wire_format_is_stable() {
1580        let msg = InboundDelegateMsg::WakeupFired {
1581            tag: vec![0xAA, 0xBB],
1582        };
1583        let encoded = bincode::serialize(&msg).unwrap();
1584
1585        // tag 9 (u32 LE) + Vec<u8> len (u64 LE = 2) + the two tag bytes.
1586        let mut expected = vec![9u8, 0, 0, 0];
1587        expected.extend_from_slice(&[2, 0, 0, 0, 0, 0, 0, 0]);
1588        expected.extend_from_slice(&[0xAA, 0xBB]);
1589        assert_eq!(
1590            encoded, expected,
1591            "WakeupFired must stay at variant tag 9 with a stable payload layout"
1592        );
1593
1594        let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&encoded).unwrap();
1595        assert!(matches!(
1596            decoded,
1597            InboundDelegateMsg::WakeupFired { tag } if tag == vec![0xAA, 0xBB]
1598        ));
1599    }
1600}
1601
1602/// Executable evidence for the wire-compatibility rules documented on
1603/// [`InboundDelegateMsg`] and [`OutboundDelegateMsg`].
1604///
1605/// The claims those doc comments make about bincode's behaviour are asserted
1606/// here rather than believed, because every one of them is the kind of claim
1607/// that is easy to state, easy to get backwards, and impossible to notice being
1608/// wrong until deployed delegate WASM misreads a message in production.
1609#[cfg(test)]
1610mod delegate_wire_compat {
1611    use super::*;
1612    use crate::contract_interface::WrappedContract;
1613    use crate::prelude::ContractCode;
1614    use crate::versioning::ContractWasmAPIVersion;
1615    use std::sync::Arc;
1616
1617    /// The number of variants each enum has **today**. These are not free
1618    /// parameters: see `an_unpinned_variant_fails_this_test`, which is what
1619    /// makes them fail closed rather than drift.
1620    const INBOUND_VARIANT_COUNT: u32 = 11;
1621    const OUTBOUND_VARIANT_COUNT: u32 = 9;
1622
1623    fn instance_id() -> ContractInstanceId {
1624        ContractInstanceId::new([0x5Au8; 32])
1625    }
1626
1627    fn delegate_key() -> DelegateKey {
1628        DelegateKey::new([0x11u8; 32], CodeHash::new([0x22u8; 32]))
1629    }
1630
1631    fn contract_container() -> ContractContainer {
1632        ContractContainer::Wasm(ContractWasmAPIVersion::V1(WrappedContract::new(
1633            Arc::new(ContractCode::from(vec![1u8, 2, 3])),
1634            Parameters::from(vec![9u8, 8, 7]),
1635        )))
1636    }
1637
1638    /// The bincode variant tag actually on the wire: a 4-byte little-endian
1639    /// u32 prefix (this workspace's bincode config uses fixint encoding).
1640    fn wire_tag(encoded: &[u8]) -> u32 {
1641        u32::from_le_bytes(
1642            encoded[..4]
1643                .try_into()
1644                .expect("a bincode enum encoding starts with a 4-byte tag"),
1645        )
1646    }
1647
1648    /// The tag each [`InboundDelegateMsg`] variant is frozen at, forever.
1649    ///
1650    /// This match is **exhaustive on purpose**. `#[non_exhaustive]` has no
1651    /// effect inside the crate that defines the enum, so adding a variant
1652    /// without adding an arm here is a **compile error** — which is the point.
1653    /// A new variant cannot slip in unpinned.
1654    ///
1655    /// If you are here because you added a variant: give it the next unused
1656    /// number, append it at the END of the enum, add it to `every_inbound`
1657    /// below, and bump `INBOUND_VARIANT_COUNT`. Do not renumber anything.
1658    fn pinned_inbound_tag(msg: &InboundDelegateMsg<'_>) -> u32 {
1659        match msg {
1660            InboundDelegateMsg::ApplicationMessage(_) => 0,
1661            InboundDelegateMsg::UserResponse(_) => 1,
1662            InboundDelegateMsg::GetContractResponse(_) => 2,
1663            InboundDelegateMsg::PutContractResponse(_) => 3,
1664            InboundDelegateMsg::UpdateContractResponse(_) => 4,
1665            InboundDelegateMsg::SubscribeContractResponse(_) => 5,
1666            InboundDelegateMsg::ContractNotification(_) => 6,
1667            InboundDelegateMsg::DelegateMessage(_) => 7,
1668            InboundDelegateMsg::UnsubscribeContractResponse(_) => 8,
1669            InboundDelegateMsg::WakeupFired { .. } => 9,
1670            InboundDelegateMsg::Lifecycle(_) => 10,
1671        }
1672    }
1673
1674    /// The tag each [`OutboundDelegateMsg`] variant is frozen at, forever.
1675    /// Exhaustive for the same reason as [`pinned_inbound_tag`].
1676    fn pinned_outbound_tag(msg: &OutboundDelegateMsg) -> u32 {
1677        match msg {
1678            OutboundDelegateMsg::ApplicationMessage(_) => 0,
1679            OutboundDelegateMsg::RequestUserInput(_) => 1,
1680            OutboundDelegateMsg::ContextUpdated(_) => 2,
1681            OutboundDelegateMsg::GetContractRequest(_) => 3,
1682            OutboundDelegateMsg::PutContractRequest(_) => 4,
1683            OutboundDelegateMsg::UpdateContractRequest(_) => 5,
1684            OutboundDelegateMsg::SubscribeContractRequest(_) => 6,
1685            OutboundDelegateMsg::SendDelegateMessage(_) => 7,
1686            OutboundDelegateMsg::UnsubscribeContractRequest(_) => 8,
1687        }
1688    }
1689
1690    /// One value of every [`InboundDelegateMsg`] variant.
1691    fn every_inbound() -> Vec<InboundDelegateMsg<'static>> {
1692        let id = instance_id();
1693        let ctx = DelegateContext::default();
1694        vec![
1695            InboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC])),
1696            InboundDelegateMsg::UserResponse(UserInputResponse {
1697                request_id: 7,
1698                response: ClientResponse::new(vec![0x01]),
1699                context: ctx.clone(),
1700            }),
1701            InboundDelegateMsg::GetContractResponse(GetContractResponse {
1702                contract_id: id,
1703                state: None,
1704                context: ctx.clone(),
1705            }),
1706            InboundDelegateMsg::PutContractResponse(PutContractResponse {
1707                contract_id: id,
1708                result: Ok(()),
1709                context: ctx.clone(),
1710            }),
1711            InboundDelegateMsg::UpdateContractResponse(UpdateContractResponse {
1712                contract_id: id,
1713                result: Ok(()),
1714                context: ctx.clone(),
1715            }),
1716            InboundDelegateMsg::SubscribeContractResponse(SubscribeContractResponse {
1717                contract_id: id,
1718                result: Ok(()),
1719                context: ctx.clone(),
1720            }),
1721            InboundDelegateMsg::ContractNotification(ContractNotification {
1722                contract_id: id,
1723                new_state: WrappedState::new(vec![0xAB]),
1724                context: ctx.clone(),
1725            }),
1726            InboundDelegateMsg::DelegateMessage(DelegateMessage::new(
1727                delegate_key(),
1728                delegate_key(),
1729                vec![0xEE],
1730            )),
1731            InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
1732                contract_id: id,
1733                result: Ok(()),
1734                context: ctx.clone(),
1735            }),
1736            InboundDelegateMsg::WakeupFired {
1737                tag: vec![0xAA, 0xBB],
1738            },
1739            InboundDelegateMsg::Lifecycle(crate::delegate_manifest::LifecycleEvent::Installed),
1740        ]
1741    }
1742
1743    /// One value of every [`OutboundDelegateMsg`] variant.
1744    ///
1745    /// Every variant is covered, `PutContractRequest` included: building a
1746    /// `ContractContainer` is four lines (see `contract_container`), and a pin
1747    /// test with a hole in it is exactly the shape of guard that reads as
1748    /// coverage while providing none.
1749    fn every_outbound() -> Vec<OutboundDelegateMsg> {
1750        let id = instance_id();
1751        vec![
1752            OutboundDelegateMsg::ApplicationMessage(ApplicationMessage::new(vec![0xCC])),
1753            OutboundDelegateMsg::RequestUserInput(UserInputRequest {
1754                request_id: 7,
1755                message: NotificationMessage(Cow::Owned(vec![0x02])),
1756                responses: vec![],
1757            }),
1758            OutboundDelegateMsg::ContextUpdated(DelegateContext::default()),
1759            OutboundDelegateMsg::GetContractRequest(GetContractRequest::new(id)),
1760            OutboundDelegateMsg::PutContractRequest(PutContractRequest::new(
1761                contract_container(),
1762                WrappedState::new(vec![0xAB]),
1763                RelatedContracts::default(),
1764            )),
1765            OutboundDelegateMsg::UpdateContractRequest(UpdateContractRequest::new(
1766                id,
1767                UpdateData::State(vec![0xAB].into()),
1768            )),
1769            OutboundDelegateMsg::SubscribeContractRequest(SubscribeContractRequest::new(id)),
1770            OutboundDelegateMsg::SendDelegateMessage(DelegateMessage::new(
1771                delegate_key(),
1772                delegate_key(),
1773                vec![0xEE],
1774            )),
1775            OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest::new(id)),
1776        ]
1777    }
1778
1779    /// Pins the bincode variant tag of **every** variant of both delegate
1780    /// message enums.
1781    ///
1782    /// The pin this replaces covered `InboundDelegateMsg`'s variant 0 alone, so
1783    /// any reorder that happened to leave `ApplicationMessage` first — swapping
1784    /// `UserResponse` and `GetContractResponse`, say — went undetected. That is
1785    /// not a theoretical gap: exactly that swap was written, and staged, during
1786    /// the work that produced this test.
1787    ///
1788    /// A reorder is the dangerous edit precisely because it is silent. The
1789    /// bytes still decode. They decode into the wrong variant, and the failure
1790    /// surfaces as a delegate acting on a message it was never sent.
1791    ///
1792    /// **If this test fails, do not update the expected numbers.** Either a
1793    /// variant was inserted or reordered (revert it; append instead), or one
1794    /// was removed — which reassigns every later tag and is a wire break
1795    /// needing a deliberate release decision. See the
1796    /// `RegisterDelegateWithPredecessors` removal in 0.9.0 for the shape of
1797    /// that decision: it was appended last specifically so that removing it
1798    /// renumbered nothing.
1799    #[test]
1800    fn delegate_msg_variant_tags_are_pinned() {
1801        for msg in every_inbound() {
1802            let expected = pinned_inbound_tag(&msg);
1803            let encoded = bincode::serialize(&msg).expect("inbound must serialize");
1804            assert_eq!(
1805                wire_tag(&encoded),
1806                expected,
1807                "InboundDelegateMsg::{msg:?} moved off wire tag {expected}; inserting, \
1808                 reordering or removing variants breaks deployed delegate WASM"
1809            );
1810        }
1811
1812        for msg in every_outbound() {
1813            let expected = pinned_outbound_tag(&msg);
1814            let encoded = bincode::serialize(&msg).expect("outbound must serialize");
1815            assert_eq!(
1816                wire_tag(&encoded),
1817                expected,
1818                "OutboundDelegateMsg::{msg:?} moved off wire tag {expected}; inserting, \
1819                 reordering or removing variants breaks deployed delegate WASM"
1820            );
1821        }
1822    }
1823
1824    /// Every variant is actually exercised by the pin above.
1825    ///
1826    /// [`pinned_inbound_tag`] is exhaustive, so a new variant cannot be left
1827    /// unpinned without a compile error — but it *could* be left out of
1828    /// `every_inbound`, and then the pin would silently stop covering it.
1829    /// Asserting that the sampled tags are exactly `0..COUNT`, with no gaps and
1830    /// no repeats, closes that.
1831    #[test]
1832    fn every_variant_is_covered_by_the_pin() {
1833        let mut inbound: Vec<u32> = every_inbound().iter().map(pinned_inbound_tag).collect();
1834        inbound.sort_unstable();
1835        assert_eq!(
1836            inbound,
1837            (0..INBOUND_VARIANT_COUNT).collect::<Vec<_>>(),
1838            "every_inbound must contain each InboundDelegateMsg variant exactly once"
1839        );
1840
1841        let mut outbound: Vec<u32> = every_outbound().iter().map(pinned_outbound_tag).collect();
1842        outbound.sort_unstable();
1843        assert_eq!(
1844            outbound,
1845            (0..OUTBOUND_VARIANT_COUNT).collect::<Vec<_>>(),
1846            "every_outbound must contain each OutboundDelegateMsg variant exactly once"
1847        );
1848    }
1849
1850    /// The count constants above cannot be allowed to drift, so this probes the
1851    /// enums themselves: a payload whose tag is one past the last known variant
1852    /// must fail to decode.
1853    ///
1854    /// This is the test that fails **closed**. Add a variant and forget
1855    /// everything else here, and the tag that was previously undecodable
1856    /// becomes decodable, and this fails. Without it, `INBOUND_VARIANT_COUNT`
1857    /// would be a number asserted only against a list written by the same hand
1858    /// in the same commit — which is not a check, it is a restatement.
1859    ///
1860    /// The payload is a run of zero bytes after the tag, which decodes as
1861    /// empty vectors, `None`, `Ok`, `false` and zeroed arrays, so it satisfies
1862    /// essentially any variant shape a new variant is likely to have. Trailing
1863    /// bytes are ignored: `bincode::deserialize` configures
1864    /// `allow_trailing_bytes()` (bincode-1.3.3 `src/lib.rs`), which is also why
1865    /// a fixed-size probe is safe here.
1866    #[test]
1867    fn an_unpinned_variant_fails_this_test() {
1868        // The probe must fail because the TAG is unknown, not because a
1869        // payload of zeros happened not to parse. Asserting only `is_err()`
1870        // would let a new variant whose first field rejects zeros (a
1871        // `DateTime`, a `NonZero*`, a validating `deserialize_with`) go
1872        // undetected: the tag would be valid, the decode would still fail, and
1873        // this test would stay green while the counts drifted.
1874        //
1875        // bincode hands an out-of-range variant index to serde's derived
1876        // visitor, which rejects it as `invalid value: integer `N`, expected
1877        // variant index 0 <= i < M` — an `ErrorKind::Custom`. Match on that
1878        // wording rather than on `InvalidTagEncoding`, which bincode produces
1879        // only for a bad `Option` discriminant.
1880        fn assert_rejected_as_unknown_variant(err: &bincode::Error, tag: u32, which: &str) {
1881            let msg = err.to_string();
1882            assert!(
1883                msg.contains("variant index"),
1884                "tag {tag} on {which} failed for the wrong reason ({msg}); the tag itself must \
1885                 still be unknown, otherwise a variant was added without updating the count, \
1886                 the pinned_*_tag match and the every_* list"
1887            );
1888        }
1889
1890        let mut probe = INBOUND_VARIANT_COUNT.to_le_bytes().to_vec();
1891        probe.extend_from_slice(&[0u8; 256]);
1892        let err = match bincode::deserialize::<InboundDelegateMsg<'_>>(&probe) {
1893            Ok(v) => panic!(
1894                "tag {INBOUND_VARIANT_COUNT} must not decode as an InboundDelegateMsg, got {v:?}"
1895            ),
1896            Err(e) => e,
1897        };
1898        assert_rejected_as_unknown_variant(&err, INBOUND_VARIANT_COUNT, "InboundDelegateMsg");
1899
1900        let mut probe = OUTBOUND_VARIANT_COUNT.to_le_bytes().to_vec();
1901        probe.extend_from_slice(&[0u8; 256]);
1902        let err = match bincode::deserialize::<OutboundDelegateMsg>(&probe) {
1903            Ok(v) => panic!(
1904                "tag {OUTBOUND_VARIANT_COUNT} must not decode as an OutboundDelegateMsg, got {v:?}"
1905            ),
1906            Err(e) => e,
1907        };
1908        assert_rejected_as_unknown_variant(&err, OUTBOUND_VARIANT_COUNT, "OutboundDelegateMsg");
1909
1910        // Control, so the probe cannot pass vacuously from the other end: the
1911        // LAST known tag must still decode from the same all-zero payload. If
1912        // this ever fails, the zero payload has stopped being a valid encoding
1913        // for the final variant, and the probes above are no longer testing
1914        // what they claim.
1915        let mut control = (INBOUND_VARIANT_COUNT - 1).to_le_bytes().to_vec();
1916        control.extend_from_slice(&[0u8; 256]);
1917        bincode::deserialize::<InboundDelegateMsg<'_>>(&control).expect(
1918            "the LAST inbound variant's payload must be decodable from zeros, or this probe can \
1919             no longer tell an unknown tag from an unparseable payload. If a variant whose \
1920             payload rejects zeros was just appended, do not delete this — point the control at \
1921             a variant that still decodes from zeros",
1922        );
1923
1924        let mut control = (OUTBOUND_VARIANT_COUNT - 1).to_le_bytes().to_vec();
1925        control.extend_from_slice(&[0u8; 256]);
1926        bincode::deserialize::<OutboundDelegateMsg>(&control).expect(
1927            "the LAST outbound variant's payload must be decodable from zeros — see the inbound \
1928             control above for what to do if that stops being true",
1929        );
1930    }
1931
1932    /// Direction 1 of the append rule: **old sender to new receiver works.**
1933    ///
1934    /// The payload is hand-built rather than produced by this crate's own
1935    /// encoder, so it stands in for bytes emitted by a delegate compiled
1936    /// against an older stdlib; an encoder-produced value would only prove the
1937    /// code agrees with itself.
1938    ///
1939    /// Named for what it actually pins. Nothing here appends a variant — the
1940    /// test cannot fail *because of* an append, only because a tag moved or a
1941    /// payload layout changed, which `delegate_msg_variant_tags_are_pinned`
1942    /// also covers. Its distinct value is that the expected bytes are written
1943    /// out by hand, so a change to `ContractNotification`'s field order or to
1944    /// the bincode config fails here with a concrete byte string to compare
1945    /// against. Direction 2, which genuinely models an old receiver, is
1946    /// `a_new_variant_does_not_decode_on_an_old_receiver` below.
1947    #[test]
1948    fn a_hand_built_old_encoder_payload_decodes_into_the_same_variant() {
1949        // InboundDelegateMsg tag 6 = ContractNotification { contract_id,
1950        // new_state: WrappedState (empty), context: DelegateContext (empty) }.
1951        let mut old_payload = vec![6u8, 0, 0, 0];
1952        old_payload.extend_from_slice(&[0x5Au8; 32]);
1953        old_payload.extend_from_slice(&0u64.to_le_bytes()); // new_state: len 0
1954        old_payload.extend_from_slice(&0u64.to_le_bytes()); // context: len 0
1955
1956        let decoded: InboundDelegateMsg<'_> = bincode::deserialize(&old_payload)
1957            .expect("a payload predating any appended variant must still decode");
1958        match decoded {
1959            InboundDelegateMsg::ContractNotification(n) => {
1960                assert_eq!(n.contract_id, instance_id());
1961            }
1962            other => panic!("an old ContractNotification decoded as {other:?}"),
1963        }
1964    }
1965
1966    /// Direction 2 of the append rule: **new sender to old receiver fails, and
1967    /// fails loudly.** This is the direction the docs warn about, so it is
1968    /// asserted rather than assumed.
1969    ///
1970    /// An old receiver is modelled by an enum with a truncated tag space,
1971    /// which is exactly what an older stdlib's version of these types is. The
1972    /// point is that the failure is an `Err` — not a silent mis-decode into
1973    /// whatever variant happens to sit at that index.
1974    #[test]
1975    fn a_new_variant_does_not_decode_on_an_old_receiver() {
1976        // An "old" OutboundDelegateMsg that knows tags 0..=6 only, i.e. one
1977        // built before `SendDelegateMessage` was appended at 7.
1978        // Variants are only ever produced by deserialization, never
1979        // constructed here — which is the whole point of the test.
1980        #[allow(dead_code)]
1981        #[derive(serde::Deserialize, Debug)]
1982        enum OldOutboundTagSpace {
1983            V0,
1984            V1,
1985            V2,
1986            V3,
1987            V4,
1988            V5,
1989            V6,
1990        }
1991
1992        let new_msg = bincode::serialize(&OutboundDelegateMsg::SendDelegateMessage(
1993            DelegateMessage::new(delegate_key(), delegate_key(), vec![0xEE]),
1994        ))
1995        .expect("outbound must serialize");
1996        assert_eq!(wire_tag(&new_msg), 7);
1997
1998        let decoded = bincode::deserialize::<OldOutboundTagSpace>(&new_msg);
1999        assert!(
2000            decoded.is_err(),
2001            "a receiver that predates a variant must REJECT it, not mis-decode it; \
2002             if this ever passes, the compatibility rule documented on \
2003             OutboundDelegateMsg is wrong and delegates are silently misreading messages"
2004        );
2005    }
2006
2007    /// The unsubscribe pair added in 0.10.0 round-trips, and adding it did not
2008    /// disturb any payload that predates it.
2009    ///
2010    /// The pre-0.10.0 byte string is hand-built rather than produced by this
2011    /// crate, so it stands in for bytes from a delegate compiled before the
2012    /// pair existed. Both halves matter: the new variant must work, and the old
2013    /// ones must be untouched by its arrival.
2014    #[test]
2015    fn the_unsubscribe_pair_round_trips_and_disturbs_nothing_older() {
2016        let id = instance_id();
2017
2018        let req =
2019            OutboundDelegateMsg::UnsubscribeContractRequest(UnsubscribeContractRequest::new(id));
2020        let encoded = bincode::serialize(&req).expect("request must serialize");
2021        assert_eq!(wire_tag(&encoded), 8, "unsubscribe request is frozen at 8");
2022        match bincode::deserialize::<OutboundDelegateMsg>(&encoded).expect("must round-trip") {
2023            OutboundDelegateMsg::UnsubscribeContractRequest(r) => {
2024                assert_eq!(r.contract_id, id);
2025                assert!(!r.processed);
2026            }
2027            other => panic!("round-tripped into {other:?}"),
2028        }
2029
2030        let resp = InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
2031            contract_id: id,
2032            result: Ok(()),
2033            context: DelegateContext::default(),
2034        });
2035        let encoded = bincode::serialize(&resp).expect("response must serialize");
2036        assert_eq!(wire_tag(&encoded), 8, "unsubscribe response is frozen at 8");
2037        match bincode::deserialize::<InboundDelegateMsg<'_>>(&encoded).expect("must round-trip") {
2038            InboundDelegateMsg::UnsubscribeContractResponse(r) => {
2039                // Assert the VALUES, not merely the variant. Checking only
2040                // `matches!` is what lets a field reorder through: the encoder
2041                // and decoder would still agree with each other.
2042                assert_eq!(r.contract_id, id);
2043                assert!(r.result.is_ok());
2044            }
2045            other => panic!("round-tripped into {other:?}"),
2046        }
2047
2048        // Both structs' doc comments say the field ORDER is the wire format.
2049        // A round-trip through this crate's own encoder cannot establish that —
2050        // it proves the code agrees with itself, and a swap of `contract_id`
2051        // and `result` would round-trip just as happily. So the layout is
2052        // frozen as hand-written bytes, the same way ContractNotification is.
2053        let mut expected_resp = vec![8u8, 0, 0, 0];
2054        expected_resp.extend_from_slice(&[0x5Au8; 32]); // contract_id
2055        expected_resp.extend_from_slice(&0u32.to_le_bytes()); // result: Ok variant tag
2056        expected_resp.extend_from_slice(&0u64.to_le_bytes()); // context: empty
2057        assert_eq!(
2058            encoded, expected_resp,
2059            "UnsubscribeContractResponse layout is frozen: tag, contract_id, result, context"
2060        );
2061
2062        let expected_req = {
2063            let mut v = vec![8u8, 0, 0, 0];
2064            v.extend_from_slice(&[0x5Au8; 32]); // contract_id
2065            v.extend_from_slice(&0u64.to_le_bytes()); // context: empty
2066            v.push(0u8); // processed: false
2067            v
2068        };
2069        assert_eq!(
2070            bincode::serialize(&req).expect("request must serialize"),
2071            expected_req,
2072            "UnsubscribeContractRequest layout is frozen: tag, contract_id, context, processed"
2073        );
2074
2075        // The error path has a different bincode shape from Ok and is part of
2076        // the same frozen layout, so it is exercised rather than assumed.
2077        let err_resp =
2078            InboundDelegateMsg::UnsubscribeContractResponse(UnsubscribeContractResponse {
2079                contract_id: id,
2080                result: Err("nope".to_string()),
2081                context: DelegateContext::default(),
2082            });
2083        match bincode::deserialize::<InboundDelegateMsg<'_>>(
2084            &bincode::serialize(&err_resp).expect("must serialize"),
2085        )
2086        .expect("must round-trip")
2087        {
2088            InboundDelegateMsg::UnsubscribeContractResponse(r) => {
2089                assert_eq!(r.result.unwrap_err(), "nope");
2090            }
2091            other => panic!("error response round-tripped into {other:?}"),
2092        }
2093
2094        // A ContractNotification encoded before 0.10.0 existed: tag 6, the 32
2095        // raw id bytes, an empty state and an empty context. Appending at 8
2096        // must leave it decoding exactly as it always did.
2097        let mut pre_0_9_0 = vec![6u8, 0, 0, 0];
2098        pre_0_9_0.extend_from_slice(&[0x5Au8; 32]);
2099        pre_0_9_0.extend_from_slice(&0u64.to_le_bytes());
2100        pre_0_9_0.extend_from_slice(&0u64.to_le_bytes());
2101        match bincode::deserialize::<InboundDelegateMsg<'_>>(&pre_0_9_0)
2102            .expect("a pre-0.10.0 payload must still decode")
2103        {
2104            InboundDelegateMsg::ContractNotification(n) => assert_eq!(n.contract_id, id),
2105            other => panic!("a pre-0.10.0 ContractNotification decoded as {other:?}"),
2106        }
2107    }
2108
2109    /// Every inbound variant whose payload carries a `context` must return it.
2110    ///
2111    /// Both `get_context` and `get_mut_context` end in `_ => None`, so a
2112    /// missing arm is not a compile error — it silently reports "no context".
2113    /// That wildcard had already swallowed one: `UserResponse` carries a
2114    /// context and returned `None` for it, undetected, because nothing in the
2115    /// crate called either accessor.
2116    ///
2117    /// Driven off `every_inbound`, so a newly appended variant is covered the
2118    /// moment it is added to that list — which the tag pin already forces.
2119    #[test]
2120    fn every_inbound_variant_with_a_context_exposes_it() {
2121        for mut msg in every_inbound() {
2122            let tag = pinned_inbound_tag(&msg);
2123
2124            // `WakeupFired` is the one inbound variant with no context field,
2125            // and it is named here rather than skipped by a wildcard, matching
2126            // the outbound test below. See `get_context` for why it has none:
2127            // a context is per-conversation working state handed back on a
2128            // reply, and a wakeup opens a conversation rather than continuing
2129            // one. Carrying one would commit the host to persisting delegate
2130            // context across arbitrary wall-clock time, which is #5467 Phase 3.
2131            //
2132            // This asserts the accessor returns `None`, not that the struct
2133            // lacks a field. That distinction is the point: the claim "every
2134            // variant carries a context" was already false of this accessor in
2135            // 0.8.5, where it omitted `UserResponse` behind a `_ => None`
2136            // wildcard. Pin the behaviour, not the shape.
2137            if matches!(
2138                msg,
2139                InboundDelegateMsg::WakeupFired { .. } | InboundDelegateMsg::Lifecycle(_)
2140            ) {
2141                assert!(
2142                    msg.get_context().is_none() && msg.get_mut_context().is_none(),
2143                    "WakeupFired and Lifecycle are documented as carrying no context; if one grew one,                      remove this exemption rather than widening it"
2144                );
2145                continue;
2146            }
2147
2148            assert!(
2149                msg.get_context().is_some(),
2150                "InboundDelegateMsg tag {tag} has a context field but get_context returned None; \
2151                 the `_ => None` wildcard hides a missing arm"
2152            );
2153            assert!(
2154                msg.get_mut_context().is_some(),
2155                "InboundDelegateMsg tag {tag} has a context field but get_mut_context returned \
2156                 None; the two accessors must agree"
2157            );
2158        }
2159    }
2160
2161    /// The same, for the outbound side.
2162    ///
2163    /// `RequestUserInput` and `ContextUpdated` genuinely have no context field
2164    /// to return, so they are the two exceptions and are named explicitly
2165    /// rather than skipped by a wildcard.
2166    #[test]
2167    fn every_outbound_variant_with_a_context_exposes_it() {
2168        for mut msg in every_outbound() {
2169            let tag = pinned_outbound_tag(&msg);
2170            let has_no_context = matches!(
2171                msg,
2172                OutboundDelegateMsg::RequestUserInput(_) | OutboundDelegateMsg::ContextUpdated(_)
2173            );
2174            if has_no_context {
2175                continue;
2176            }
2177            assert!(
2178                msg.get_context().is_some(),
2179                "OutboundDelegateMsg tag {tag} has a context field but get_context returned None"
2180            );
2181            assert!(
2182                msg.get_mut_context().is_some(),
2183                "OutboundDelegateMsg tag {tag} has a context field but get_mut_context returned \
2184                 None; the two accessors must agree"
2185            );
2186        }
2187    }
2188
2189    // ---------------------------------------------------------------------
2190    // `#[serde(other)]` — the one rule in WIRE-FORMAT.md that contradicts the
2191    // common advice, so it is the one a future reader will doubt and re-derive.
2192    // These three tests are that derivation, kept where it cannot rot.
2193    //
2194    // Mock types, deliberately: the real enums must never grow a catch-all, so
2195    // the property has to be demonstrated on stand-ins.
2196    // ---------------------------------------------------------------------
2197
2198    // The appended variants sit at tag 2, and `OldMsgWithCatchAll` declares
2199    // only 0 and 1. That gap is load-bearing: at tag 1 the catch-all's own
2200    // declared index, a plain unit variant decodes identically and the
2201    // attribute does no work at all — so mocks aligned that way pass with
2202    // `#[serde(other)]` deleted, testing nothing. Verified: they did.
2203    //
2204    // Both cases occur on a real append. The FIRST new variant lands exactly at
2205    // the catch-all's index, where the attribute is unnecessary; the SECOND is
2206    // out of range, where it is the only thing between a hard error and silent
2207    // corruption. The out-of-range case is the one the rule depends on, so it
2208    // is the one the mocks must produce.
2209    #[derive(Serialize, Deserialize, Debug, PartialEq)]
2210    enum NewMsgWithPayload {
2211        First(u32),
2212        Second(bool),
2213        Appended(String),
2214    }
2215
2216    #[derive(Serialize, Deserialize, Debug, PartialEq)]
2217    enum OldMsgWithCatchAll {
2218        First(u32),
2219        // Deliberately stops here: real variants 0 only, catch-all at 1. The
2220        // appended variants above are at tag 2, which is OUT OF RANGE for this
2221        // enum — that gap is what the attribute has to bridge.
2222        #[serde(other)]
2223        Unknown,
2224    }
2225
2226    #[derive(Serialize, Deserialize, Debug, PartialEq)]
2227    enum NewMsgUnitAppended {
2228        First(u32),
2229        Second(bool),
2230        AppendedUnit,
2231    }
2232
2233    /// The mocks' tag gap is asserted, not merely commented — and asserted
2234    /// against **the two enums whose alignment actually matters**.
2235    ///
2236    /// The vacuity condition is precisely: the tag `Appended` encodes to is the
2237    /// same as the index `OldMsgWithCatchAll` absorbs into `Unknown`. At that
2238    /// index a plain unit variant behaves identically and `#[serde(other)]`
2239    /// does no work, so the three tests below stop testing the attribute while
2240    /// still passing.
2241    ///
2242    /// Both numbers are measured from the types rather than written down, so
2243    /// this fires whichever side moves — adding a variant to the old enum, or
2244    /// removing the filler from the new ones. An earlier version of this guard
2245    /// compared against a separate no-attribute copy of the old enum and
2246    /// **missed the first case entirely**, because that copy did not move when
2247    /// the real one did. A control that can drift from what it controls is not
2248    /// a control.
2249    ///
2250    /// This exists because the alignment has broken **three times** in this
2251    /// file, twice at the hands of someone actively fixing it. A comment cannot
2252    /// catch the fourth.
2253    #[test]
2254    fn the_attribute_is_what_bridges_the_gap() {
2255        fn tag_of(bytes: &[u8]) -> u32 {
2256            u32::from_le_bytes(
2257                bytes[..4]
2258                    .try_into()
2259                    .expect("a bincode enum tag is 4 bytes"),
2260            )
2261        }
2262
2263        let appended =
2264            tag_of(&bincode::serialize(&NewMsgWithPayload::Appended("x".into())).unwrap());
2265
2266        // The lowest tag `OldMsgWithCatchAll` absorbs into `Unknown` is its
2267        // catch-all index; below it, real variants decode as themselves.
2268        let absorbed_from = (0u32..16)
2269            .find(|t| {
2270                let mut probe = t.to_le_bytes().to_vec();
2271                probe.extend_from_slice(&[0u8; 32]);
2272                matches!(
2273                    bincode::deserialize::<OldMsgWithCatchAll>(&probe),
2274                    Ok(OldMsgWithCatchAll::Unknown)
2275                )
2276            })
2277            .expect("OldMsgWithCatchAll must absorb some tag; it has #[serde(other)]");
2278
2279        assert!(
2280            appended > absorbed_from,
2281            "`Appended` is at tag {appended} and OldMsgWithCatchAll absorbs from tag \
2282             {absorbed_from}: the mocks have re-aligned, so the serde(other) tests below \
2283             are vacuous and pass with the attribute deleted. Move `Appended` above the \
2284             catch-all index again rather than adjusting this test."
2285        );
2286    }
2287
2288    /// Contradicts the usual "self-describing formats only" claim: bincode 1.x
2289    /// **does** let `#[serde(other)]` absorb an unknown variant tag.
2290    ///
2291    /// That is the trap, not a feature — see the next test for why.
2292    #[test]
2293    fn serde_other_does_absorb_an_unknown_tag_in_bincode() {
2294        let encoded = bincode::serialize(&NewMsgWithPayload::Appended("x".into())).unwrap();
2295        let decoded: OldMsgWithCatchAll =
2296            bincode::deserialize(&encoded).expect("serde(other) absorbs the unknown tag");
2297        assert_eq!(decoded, OldMsgWithCatchAll::Unknown);
2298    }
2299
2300    /// The absorption consumes the **tag only**, never the unknown variant's
2301    /// payload, so everything after it in the buffer is silently misread.
2302    ///
2303    /// A hard decode error would have been strictly better: this turns a loud,
2304    /// immediate failure into a wrong value with no error anywhere.
2305    #[test]
2306    fn the_catch_all_silently_corrupts_trailing_data() {
2307        let encoded =
2308            bincode::serialize(&(NewMsgWithPayload::Appended("hello-future".into()), 4242u32))
2309                .unwrap();
2310
2311        let (variant, trailing): (OldMsgWithCatchAll, u32) =
2312            bincode::deserialize(&encoded).expect("decodes, which is the problem");
2313
2314        assert_eq!(variant, OldMsgWithCatchAll::Unknown);
2315        assert_ne!(
2316            trailing, 4242,
2317            "if this ever equals 4242, serde(other) stopped eating the payload \
2318             and this section of WIRE-FORMAT.md needs revisiting"
2319        );
2320    }
2321
2322    /// And the reason the trap works: against a **unit** unknown variant there
2323    /// is no payload to leave behind, nothing after it is misread, and the
2324    /// decode really is clean.
2325    ///
2326    /// So a developer who tries `#[serde(other)]` on a unit variant sees it
2327    /// work and concludes the warning is overstated. The corruption is
2328    /// conditional on a property of a variant that does not exist yet — you are
2329    /// betting nobody ever gives a future variant a field.
2330    #[test]
2331    fn the_catch_all_is_clean_for_a_unit_variant() {
2332        let encoded = bincode::serialize(&(NewMsgUnitAppended::AppendedUnit, 4242u32)).unwrap();
2333
2334        let (variant, trailing): (OldMsgWithCatchAll, u32) =
2335            bincode::deserialize(&encoded).expect("unit variant leaves nothing behind");
2336
2337        assert_eq!(variant, OldMsgWithCatchAll::Unknown);
2338        assert_eq!(
2339            trailing, 4242,
2340            "a unit unknown variant must NOT corrupt what follows — this is the \
2341             case that misleads, and it is why the rule is unconditional"
2342        );
2343    }
2344}