Skip to main content

freenet_stdlib/client_api/
client_events.rs

1use bytes::Bytes;
2use flatbuffers::WIPOffset;
3use std::borrow::Cow;
4use std::fmt::Display;
5use std::net::SocketAddr;
6
7use serde::{de::DeserializeOwned, Deserialize, Serialize};
8
9use crate::client_api::TryFromFbs;
10use crate::generated::client_request::{
11    root_as_client_request, ClientRequestType, ContractRequest as FbsContractRequest,
12    ContractRequestType, DelegateRequest as FbsDelegateRequest, DelegateRequestType,
13};
14
15use crate::generated::common::{
16    ApplicationMessage as FbsApplicationMessage, ApplicationMessageArgs, ContractCode,
17    ContractCodeArgs, ContractContainer as FbsContractContainer, ContractContainerArgs,
18    ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
19    ContractKey as FbsContractKey, ContractKeyArgs, ContractType, DeltaUpdate, DeltaUpdateArgs,
20    RelatedDeltaUpdate, RelatedDeltaUpdateArgs, RelatedStateAndDeltaUpdate,
21    RelatedStateAndDeltaUpdateArgs, RelatedStateUpdate, RelatedStateUpdateArgs,
22    StateAndDeltaUpdate, StateAndDeltaUpdateArgs, StateUpdate, StateUpdateArgs,
23    UpdateData as FbsUpdateData, UpdateDataArgs, UpdateDataType, WasmContractV1,
24    WasmContractV1Args,
25};
26use crate::generated::host_response::{
27    finish_host_response_buffer, ClientResponse as FbsClientResponse, ClientResponseArgs,
28    ContextUpdated as FbsContextUpdated, ContextUpdatedArgs,
29    ContractResponse as FbsContractResponse, ContractResponseArgs, ContractResponseType,
30    DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateResponse as FbsDelegateResponse,
31    DelegateResponseArgs, GetResponse as FbsGetResponse, GetResponseArgs,
32    HostResponse as FbsHostResponse, HostResponseArgs, HostResponseType, NotFound as FbsNotFound,
33    NotFoundArgs, Ok as FbsOk, OkArgs, OutboundDelegateMsg as FbsOutboundDelegateMsg,
34    OutboundDelegateMsgArgs, OutboundDelegateMsgType, PutResponse as FbsPutResponse,
35    PutResponseArgs, RequestUserInput as FbsRequestUserInput, RequestUserInputArgs,
36    StreamChunk as FbsHostStreamChunk, StreamChunkArgs as FbsHostStreamChunkArgs,
37    UpdateNotification as FbsUpdateNotification, UpdateNotificationArgs,
38    UpdateResponse as FbsUpdateResponse, UpdateResponseArgs,
39};
40use crate::prelude::ContractContainer::Wasm;
41use crate::prelude::ContractWasmAPIVersion::V1;
42use crate::prelude::UpdateData::{
43    Delta, RelatedDelta, RelatedState, RelatedStateAndDelta, State, StateAndDelta,
44};
45use crate::{
46    delegate_interface::{DelegateKey, InboundDelegateMsg, OutboundDelegateMsg},
47    prelude::{
48        ContractInstanceId, ContractKey, DelegateContainer, Parameters, RelatedContracts,
49        SecretsId, StateSummary, UpdateData, WrappedState,
50    },
51    versioning::ContractContainer,
52};
53
54use super::WsApiError;
55
56#[derive(Debug, Serialize, Deserialize, Clone)]
57pub struct ClientError {
58    kind: Box<ErrorKind>,
59}
60
61impl ClientError {
62    pub fn into_fbs_bytes(self) -> Result<Vec<u8>, Box<ClientError>> {
63        use crate::generated::host_response::{Error, ErrorArgs};
64        let mut builder = flatbuffers::FlatBufferBuilder::new();
65        let msg_offset = builder.create_string(&self.to_string());
66        let err_offset = Error::create(
67            &mut builder,
68            &ErrorArgs {
69                msg: Some(msg_offset),
70            },
71        );
72        let host_response_offset = FbsHostResponse::create(
73            &mut builder,
74            &HostResponseArgs {
75                response_type: HostResponseType::Ok,
76                response: Some(err_offset.as_union_value()),
77            },
78        );
79        finish_host_response_buffer(&mut builder, host_response_offset);
80        Ok(builder.finished_data().to_vec())
81    }
82
83    pub fn kind(&self) -> &ErrorKind {
84        &self.kind
85    }
86}
87
88impl From<ErrorKind> for ClientError {
89    fn from(kind: ErrorKind) -> Self {
90        ClientError {
91            kind: Box::new(kind),
92        }
93    }
94}
95
96impl<T: Into<Cow<'static, str>>> From<T> for ClientError {
97    fn from(cause: T) -> Self {
98        ClientError {
99            kind: Box::new(ErrorKind::Unhandled {
100                cause: cause.into(),
101            }),
102        }
103    }
104}
105
106#[derive(thiserror::Error, Debug, Serialize, Deserialize, Clone)]
107#[non_exhaustive]
108pub enum ErrorKind {
109    #[error("comm channel between client/host closed")]
110    ChannelClosed,
111    #[error("error while deserializing: {cause}")]
112    DeserializationError { cause: Cow<'static, str> },
113    #[error("client disconnected")]
114    Disconnect,
115    #[error("failed while trying to unpack state for {0}")]
116    IncorrectState(ContractKey),
117    #[error("node not available")]
118    NodeUnavailable,
119    #[error("lost the connection with the protocol handling connections")]
120    TransportProtocolDisconnect,
121    #[error("unhandled error: {cause}")]
122    Unhandled { cause: Cow<'static, str> },
123    #[error("unknown client id: {0}")]
124    UnknownClient(usize),
125    #[error(transparent)]
126    RequestError(#[from] RequestError),
127    #[error("error while executing operation in the network: {cause}")]
128    OperationError { cause: Cow<'static, str> },
129    // TODO: identify requests by some id so we can inform clients which one failed exactly
130    #[error("operation timed out")]
131    FailedOperation,
132    #[error("peer should shutdown")]
133    Shutdown,
134    #[error("no ring connections found")]
135    EmptyRing,
136    #[error("peer has not joined the network yet")]
137    PeerNotJoined,
138}
139
140impl Display for ClientError {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "client error: {}", self.kind)
143    }
144}
145
146impl std::error::Error for ClientError {}
147
148#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
149#[non_exhaustive]
150pub enum RequestError {
151    #[error(transparent)]
152    ContractError(#[from] ContractError),
153    #[error(transparent)]
154    DelegateError(#[from] DelegateError),
155    #[error("client disconnect")]
156    Disconnect,
157    #[error("operation timed out")]
158    Timeout,
159}
160
161/// Errors that may happen while interacting with delegates.
162#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
163#[non_exhaustive]
164pub enum DelegateError {
165    #[error("error while registering delegate {0}")]
166    RegisterError(DelegateKey),
167    #[error("execution error, cause {0}")]
168    ExecutionError(Cow<'static, str>),
169    #[error("missing delegate {0}")]
170    Missing(DelegateKey),
171    #[error("missing secret `{secret}` for delegate {key}")]
172    MissingSecret { key: DelegateKey, secret: SecretsId },
173    #[error("forbidden access to secret: {0}")]
174    ForbiddenSecretAccess(SecretsId),
175}
176
177/// Errors that may happen while interacting with contracts.
178#[derive(Debug, thiserror::Error, Serialize, Deserialize, Clone)]
179#[non_exhaustive]
180pub enum ContractError {
181    #[error("failed to get contract {key}, reason: {cause}")]
182    Get {
183        key: ContractKey,
184        cause: Cow<'static, str>,
185    },
186    #[error("put error for contract {key}, reason: {cause}")]
187    Put {
188        key: ContractKey,
189        cause: Cow<'static, str>,
190    },
191    #[error("update error for contract {key}, reason: {cause}")]
192    Update {
193        key: ContractKey,
194        cause: Cow<'static, str>,
195    },
196    #[error("failed to subscribe for contract {key}, reason: {cause}")]
197    Subscribe {
198        key: ContractKey,
199        cause: Cow<'static, str>,
200    },
201    // todo: actually build a stack of the involved keys
202    #[error("dependency contract stack overflow : {key}")]
203    ContractStackOverflow {
204        key: crate::contract_interface::ContractInstanceId,
205    },
206    #[error("missing related contract: {key}")]
207    MissingRelated {
208        key: crate::contract_interface::ContractInstanceId,
209    },
210    #[error("missing contract: {key}")]
211    MissingContract {
212        key: crate::contract_interface::ContractInstanceId,
213    },
214}
215
216impl ContractError {
217    const EXECUTION_ERROR: &'static str = "execution error";
218    const INVALID_PUT: &'static str = "invalid put";
219
220    pub fn update_exec_error(key: ContractKey, additional_info: impl std::fmt::Display) -> Self {
221        Self::Update {
222            key,
223            cause: format!(
224                "{exec_err}: {additional_info}",
225                exec_err = Self::EXECUTION_ERROR
226            )
227            .into(),
228        }
229    }
230
231    pub fn invalid_put(key: ContractKey) -> Self {
232        Self::Put {
233            key,
234            cause: Self::INVALID_PUT.into(),
235        }
236    }
237
238    pub fn invalid_update(key: ContractKey) -> Self {
239        Self::Update {
240            key,
241            cause: Self::INVALID_PUT.into(),
242        }
243    }
244}
245
246/// A request from a client application to the host.
247#[derive(Serialize, Deserialize, Debug, Clone)]
248#[non_exhaustive]
249// #[cfg_attr(test, derive(arbitrary::Arbitrary))]
250pub enum ClientRequest<'a> {
251    DelegateOp(#[serde(borrow)] DelegateRequest<'a>),
252    ContractOp(#[serde(borrow)] ContractRequest<'a>),
253    Disconnect {
254        cause: Option<Cow<'static, str>>,
255    },
256    Authenticate {
257        token: String,
258    },
259    NodeQueries(NodeQuery),
260    /// Gracefully disconnect from the host.
261    Close,
262    /// A chunk of a larger streamed message.
263    StreamChunk {
264        stream_id: u32,
265        index: u32,
266        total: u32,
267        data: Bytes,
268    },
269}
270
271#[derive(Serialize, Deserialize, Debug, Clone)]
272pub struct ConnectedPeers {}
273
274#[derive(Serialize, Deserialize, Debug, Clone)]
275pub struct NodeDiagnostics {
276    /// Optional contract key to filter diagnostics for specific contract
277    pub contract_key: Option<ContractKey>,
278}
279
280impl ClientRequest<'_> {
281    pub fn into_owned(self) -> ClientRequest<'static> {
282        match self {
283            ClientRequest::ContractOp(op) => {
284                let owned = match op {
285                    ContractRequest::Put {
286                        contract,
287                        state,
288                        related_contracts,
289                        subscribe,
290                        blocking_subscribe,
291                    } => {
292                        let related_contracts = related_contracts.into_owned();
293                        ContractRequest::Put {
294                            contract,
295                            state,
296                            related_contracts,
297                            subscribe,
298                            blocking_subscribe,
299                        }
300                    }
301                    ContractRequest::Update { key, data } => {
302                        let data = data.into_owned();
303                        ContractRequest::Update { key, data }
304                    }
305                    ContractRequest::Get {
306                        key,
307                        return_contract_code,
308                        subscribe,
309                        blocking_subscribe,
310                    } => ContractRequest::Get {
311                        key,
312                        return_contract_code,
313                        subscribe,
314                        blocking_subscribe,
315                    },
316                    ContractRequest::Subscribe { key, summary } => ContractRequest::Subscribe {
317                        key,
318                        summary: summary.map(StateSummary::into_owned),
319                    },
320                };
321                owned.into()
322            }
323            ClientRequest::DelegateOp(op) => {
324                let op = op.into_owned();
325                ClientRequest::DelegateOp(op)
326            }
327            ClientRequest::Disconnect { cause } => ClientRequest::Disconnect { cause },
328            ClientRequest::Authenticate { token } => ClientRequest::Authenticate { token },
329            ClientRequest::NodeQueries(query) => ClientRequest::NodeQueries(query),
330            ClientRequest::Close => ClientRequest::Close,
331            ClientRequest::StreamChunk {
332                stream_id,
333                index,
334                total,
335                data,
336            } => ClientRequest::StreamChunk {
337                stream_id,
338                index,
339                total,
340                data,
341            },
342        }
343    }
344
345    pub fn is_disconnect(&self) -> bool {
346        matches!(self, Self::Disconnect { .. })
347    }
348
349    pub fn try_decode_fbs(msg: &[u8]) -> Result<ClientRequest<'_>, WsApiError> {
350        let req = {
351            match root_as_client_request(msg) {
352                Ok(client_request) => match client_request.client_request_type() {
353                    ClientRequestType::ContractRequest => {
354                        let contract_request =
355                            client_request.client_request_as_contract_request().unwrap();
356                        ContractRequest::try_decode_fbs(&contract_request)?.into()
357                    }
358                    ClientRequestType::DelegateRequest => {
359                        let delegate_request =
360                            client_request.client_request_as_delegate_request().unwrap();
361                        DelegateRequest::try_decode_fbs(&delegate_request)?.into()
362                    }
363                    ClientRequestType::Disconnect => {
364                        let delegate_request =
365                            client_request.client_request_as_disconnect().unwrap();
366                        let cause = delegate_request
367                            .cause()
368                            .map(|cause_msg| cause_msg.to_string().into());
369                        ClientRequest::Disconnect { cause }
370                    }
371                    ClientRequestType::Authenticate => {
372                        let auth_req = client_request.client_request_as_authenticate().unwrap();
373                        let token = auth_req.token();
374                        ClientRequest::Authenticate {
375                            token: token.to_owned(),
376                        }
377                    }
378                    ClientRequestType::StreamChunk => {
379                        let chunk = client_request.client_request_as_stream_chunk().unwrap();
380                        ClientRequest::StreamChunk {
381                            stream_id: chunk.stream_id(),
382                            index: chunk.index(),
383                            total: chunk.total(),
384                            data: Bytes::from(chunk.data().bytes().to_vec()),
385                        }
386                    }
387                    other => {
388                        return Err(crate::client_api::unknown_union_discriminant(
389                            "ClientRequestType",
390                            other.0,
391                        ))
392                    }
393                },
394                Err(e) => {
395                    let cause = format!("{e}");
396                    return Err(WsApiError::deserialization(cause));
397                }
398            }
399        };
400
401        Ok(req)
402    }
403}
404
405#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
406#[non_exhaustive]
407pub enum ContractRequest<'a> {
408    /// Insert a new value in a contract corresponding with the provided key.
409    Put {
410        contract: ContractContainer,
411        /// Value to upsert in the contract.
412        state: WrappedState,
413        /// Related contracts.
414        #[serde(borrow)]
415        related_contracts: RelatedContracts<'a>,
416        /// If this flag is set then subscribe to updates for this contract.
417        subscribe: bool,
418        /// If true, the PUT response waits for the subscription to complete.
419        /// Only meaningful when `subscribe` is true.
420        #[serde(default)]
421        blocking_subscribe: bool,
422    },
423    /// Update an existing contract corresponding with the provided key.
424    Update {
425        key: ContractKey,
426        #[serde(borrow)]
427        data: UpdateData<'a>,
428    },
429    /// Fetch the current state from a contract corresponding to the provided key.
430    Get {
431        /// Instance ID of the contract (the hash of code + params).
432        /// Only the instance ID is needed since the client doesn't have the code hash yet.
433        key: ContractInstanceId,
434        /// If this flag is set then fetch also the contract itself.
435        return_contract_code: bool,
436        /// If this flag is set then subscribe to updates for this contract.
437        subscribe: bool,
438        /// If true, the GET response waits for the subscription to complete.
439        /// Only meaningful when `subscribe` is true.
440        #[serde(default)]
441        blocking_subscribe: bool,
442    },
443    /// Subscribe to the changes in a given contract. Implicitly starts a get operation
444    /// if the contract is not present yet.
445    Subscribe {
446        /// Instance ID of the contract.
447        key: ContractInstanceId,
448        summary: Option<StateSummary<'a>>,
449    },
450}
451
452impl ContractRequest<'_> {
453    pub fn into_owned(self) -> ContractRequest<'static> {
454        match self {
455            Self::Put {
456                contract,
457                state,
458                related_contracts,
459                subscribe,
460                blocking_subscribe,
461            } => ContractRequest::Put {
462                contract,
463                state,
464                related_contracts: related_contracts.into_owned(),
465                subscribe,
466                blocking_subscribe,
467            },
468            Self::Update { key, data } => ContractRequest::Update {
469                key,
470                data: data.into_owned(),
471            },
472            Self::Get {
473                key,
474                return_contract_code: fetch_contract,
475                subscribe,
476                blocking_subscribe,
477            } => ContractRequest::Get {
478                key,
479                return_contract_code: fetch_contract,
480                subscribe,
481                blocking_subscribe,
482            },
483            Self::Subscribe { key, summary } => ContractRequest::Subscribe {
484                key,
485                summary: summary.map(StateSummary::into_owned),
486            },
487        }
488    }
489}
490
491impl<'a> From<ContractRequest<'a>> for ClientRequest<'a> {
492    fn from(op: ContractRequest<'a>) -> Self {
493        ClientRequest::ContractOp(op)
494    }
495}
496
497/// Deserializes a `ContractRequest` from a Flatbuffers message.
498impl<'a> TryFromFbs<&FbsContractRequest<'a>> for ContractRequest<'a> {
499    fn try_decode_fbs(request: &FbsContractRequest<'a>) -> Result<Self, WsApiError> {
500        let req = {
501            match request.contract_request_type() {
502                ContractRequestType::Get => {
503                    let get = request.contract_request_as_get().unwrap();
504                    // Extract just the instance ID - GET only needs the instance ID,
505                    // not the full key (which may not be complete on the client side)
506                    let fbs_key = get.key();
507                    let key = crate::contract_interface::key::instance_id_from_fbs(
508                        "ContractKey.instance.data",
509                        fbs_key.instance().data().bytes(),
510                    )?;
511                    let fetch_contract = get.fetch_contract();
512                    let subscribe = get.subscribe();
513                    let blocking_subscribe = get.blocking_subscribe();
514                    ContractRequest::Get {
515                        key,
516                        return_contract_code: fetch_contract,
517                        subscribe,
518                        blocking_subscribe,
519                    }
520                }
521                ContractRequestType::Put => {
522                    let put = request.contract_request_as_put().unwrap();
523                    let contract = ContractContainer::try_decode_fbs(&put.container())?;
524                    let state = WrappedState::new(put.wrapped_state().bytes().to_vec());
525                    let related_contracts =
526                        RelatedContracts::try_decode_fbs(&put.related_contracts())?.into_owned();
527                    let subscribe = put.subscribe();
528                    let blocking_subscribe = put.blocking_subscribe();
529                    ContractRequest::Put {
530                        contract,
531                        state,
532                        related_contracts,
533                        subscribe,
534                        blocking_subscribe,
535                    }
536                }
537                ContractRequestType::Update => {
538                    let update = request.contract_request_as_update().unwrap();
539                    let key = ContractKey::try_decode_fbs(&update.key())?;
540                    let data = UpdateData::try_decode_fbs(&update.data())?.into_owned();
541                    ContractRequest::Update { key, data }
542                }
543                ContractRequestType::Subscribe => {
544                    let subscribe = request.contract_request_as_subscribe().unwrap();
545                    // Extract just the instance ID for Subscribe
546                    let fbs_key = subscribe.key();
547                    let key = crate::contract_interface::key::instance_id_from_fbs(
548                        "ContractKey.instance.data",
549                        fbs_key.instance().data().bytes(),
550                    )?;
551                    let summary = subscribe
552                        .summary()
553                        .map(|summary_data| StateSummary::from(summary_data.bytes()));
554                    ContractRequest::Subscribe { key, summary }
555                }
556                // Reachable, not `unreachable!()`: the generated flatbuffers
557                // verifier accepts any unknown union discriminant (`_ => Ok(())`)
558                // and the union type field is a raw `u8` a client can set freely,
559                // so a crafted request reaches here. Return a per-request error
560                // instead of panicking the connection handler. (Mirrors the same
561                // fix on `DelegateRequest::try_decode_fbs`.)
562                other => {
563                    return Err(crate::client_api::unknown_union_discriminant(
564                        "ContractRequestType",
565                        other.0,
566                    ));
567                }
568            }
569        };
570
571        Ok(req)
572    }
573}
574
575impl<'a> From<DelegateRequest<'a>> for ClientRequest<'a> {
576    fn from(op: DelegateRequest<'a>) -> Self {
577        ClientRequest::DelegateOp(op)
578    }
579}
580
581#[derive(Serialize, Deserialize, Debug, Clone)]
582#[non_exhaustive]
583pub enum DelegateRequest<'a> {
584    ApplicationMessages {
585        key: DelegateKey,
586        #[serde(deserialize_with = "Parameters::deser_params")]
587        params: Parameters<'a>,
588        #[serde(borrow)]
589        inbound: Vec<InboundDelegateMsg<'a>>,
590    },
591    RegisterDelegate {
592        delegate: DelegateContainer,
593        cipher: [u8; 32],
594        nonce: [u8; 24],
595    },
596    UnregisterDelegate(DelegateKey),
597    /// Registers a delegate AND requests a one-shot, node-side copy-forward of
598    /// the LOCAL-scope secrets belonging to `predecessors` into the newly
599    /// registered delegate's own secret namespace.
600    ///
601    /// `predecessors` are the delegate keys of retired generations of this
602    /// delegate. It is a **list** because a user may skip one or more
603    /// generations between the version they last ran and the current one, so
604    /// several prior namespaces can need forwarding in a single registration.
605    /// The copy-forward is:
606    ///   - **consent-gated on the client side** — the node performs NO
607    ///     execution of any old delegate WASM; it only copies the
608    ///     already-sealed LOCAL-scope secret bytes it holds for a predecessor
609    ///     into the successor's namespace,
610    ///   - **best-effort over unknown predecessors** — the server silently
611    ///     ignores any predecessor key it does not hold, and
612    ///   - **idempotent per `(predecessor -> successor)` pair** —
613    ///     re-registering with the same predecessors copies nothing new.
614    ///
615    /// `cipher` and `nonce` mirror [`DelegateRequest::RegisterDelegate`] purely
616    /// for field-shape parity. The node has ignored both since
617    /// freenet-core#4140 (secrets are sealed client-side), so they carry no
618    /// behavior here either; they exist only so the two registration variants
619    /// stay structurally identical.
620    RegisterDelegateWithPredecessors {
621        delegate: DelegateContainer,
622        cipher: [u8; 32],
623        nonce: [u8; 24],
624        predecessors: Vec<DelegateKey>,
625    },
626}
627
628impl DelegateRequest<'_> {
629    pub fn into_owned(self) -> DelegateRequest<'static> {
630        match self {
631            DelegateRequest::ApplicationMessages {
632                key,
633                inbound,
634                params,
635            } => DelegateRequest::ApplicationMessages {
636                key,
637                params: params.into_owned(),
638                inbound: inbound.into_iter().map(|e| e.into_owned()).collect(),
639            },
640            DelegateRequest::RegisterDelegate {
641                delegate,
642                cipher,
643                nonce,
644            } => DelegateRequest::RegisterDelegate {
645                delegate,
646                cipher,
647                nonce,
648            },
649            DelegateRequest::UnregisterDelegate(key) => DelegateRequest::UnregisterDelegate(key),
650            DelegateRequest::RegisterDelegateWithPredecessors {
651                delegate,
652                cipher,
653                nonce,
654                predecessors,
655            } => DelegateRequest::RegisterDelegateWithPredecessors {
656                delegate,
657                cipher,
658                nonce,
659                predecessors,
660            },
661        }
662    }
663
664    pub fn key(&self) -> &DelegateKey {
665        match self {
666            DelegateRequest::ApplicationMessages { key, .. } => key,
667            DelegateRequest::RegisterDelegate { delegate, .. } => delegate.key(),
668            DelegateRequest::UnregisterDelegate(key) => key,
669            DelegateRequest::RegisterDelegateWithPredecessors { delegate, .. } => delegate.key(),
670        }
671    }
672}
673
674impl Display for ClientRequest<'_> {
675    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
676        match self {
677            ClientRequest::ContractOp(op) => match op {
678                ContractRequest::Put {
679                    contract, state, ..
680                } => {
681                    write!(
682                        f,
683                        "ContractRequest::Put for contract `{contract}` with state {state}"
684                    )
685                }
686                ContractRequest::Update { key, .. } => write!(f, "update request for {key}"),
687                ContractRequest::Get {
688                    key,
689                    return_contract_code: contract,
690                    ..
691                } => {
692                    write!(
693                        f,
694                        "ContractRequest::Get for key `{key}` (fetch full contract: {contract})"
695                    )
696                }
697                ContractRequest::Subscribe { key, .. } => {
698                    write!(f, "ContractRequest::Subscribe for `{key}`")
699                }
700            },
701            ClientRequest::DelegateOp(op) => match op {
702                DelegateRequest::ApplicationMessages { key, inbound, .. } => {
703                    write!(
704                        f,
705                        "DelegateRequest::ApplicationMessages for `{key}` with {} messages",
706                        inbound.len()
707                    )
708                }
709                DelegateRequest::RegisterDelegate { delegate, .. } => {
710                    write!(
711                        f,
712                        "DelegateRequest::RegisterDelegate for delegate.key()=`{}`",
713                        delegate.key()
714                    )
715                }
716                DelegateRequest::UnregisterDelegate(key) => {
717                    write!(f, "DelegateRequest::UnregisterDelegate for key `{key}`")
718                }
719                DelegateRequest::RegisterDelegateWithPredecessors {
720                    delegate,
721                    predecessors,
722                    ..
723                } => {
724                    write!(
725                        f,
726                        "DelegateRequest::RegisterDelegateWithPredecessors for delegate.key()=`{}` with {} predecessor(s)",
727                        delegate.key(),
728                        predecessors.len()
729                    )
730                }
731            },
732            ClientRequest::Disconnect { .. } => write!(f, "client disconnected"),
733            ClientRequest::Authenticate { .. } => write!(f, "authenticate"),
734            ClientRequest::NodeQueries(query) => write!(f, "node queries: {:?}", query),
735            ClientRequest::Close => write!(f, "close"),
736            ClientRequest::StreamChunk {
737                stream_id,
738                index,
739                total,
740                ..
741            } => write!(f, "stream chunk {index}/{total} (stream {stream_id})"),
742        }
743    }
744}
745
746/// Deserializes a `DelegateRequest` from a Flatbuffers message.
747impl<'a> TryFromFbs<&FbsDelegateRequest<'a>> for DelegateRequest<'a> {
748    fn try_decode_fbs(request: &FbsDelegateRequest<'a>) -> Result<Self, WsApiError> {
749        let req = {
750            match request.delegate_request_type() {
751                DelegateRequestType::ApplicationMessages => {
752                    let app_msg = request.delegate_request_as_application_messages().unwrap();
753                    let key = DelegateKey::try_decode_fbs(&app_msg.key())?;
754                    let params = Parameters::from(app_msg.params().bytes());
755                    let inbound = app_msg
756                        .inbound()
757                        .iter()
758                        .map(|msg| InboundDelegateMsg::try_decode_fbs(&msg))
759                        .collect::<Result<Vec<_>, _>>()?;
760                    DelegateRequest::ApplicationMessages {
761                        key,
762                        params,
763                        inbound,
764                    }
765                }
766                DelegateRequestType::RegisterDelegate => {
767                    let register = request.delegate_request_as_register_delegate().unwrap();
768                    let delegate = DelegateContainer::try_decode_fbs(&register.delegate())?;
769                    // `cipher` and `nonce` are `(required)`, which the verifier
770                    // reads as "present", not "32 and 24 bytes". The
771                    // `try_from(..).unwrap()` this replaces panicked on any
772                    // other length, killing the connection task. (The
773                    // intermediate `.to_vec()` went with it: `bytes()` is
774                    // already a slice.)
775                    let cipher = crate::client_api::fixed_size_field::<32>(
776                        "RegisterDelegate.cipher",
777                        register.cipher().bytes(),
778                    )?;
779                    let nonce = crate::client_api::fixed_size_field::<24>(
780                        "RegisterDelegate.nonce",
781                        register.nonce().bytes(),
782                    )?;
783                    DelegateRequest::RegisterDelegate {
784                        delegate,
785                        cipher,
786                        nonce,
787                    }
788                }
789                DelegateRequestType::UnregisterDelegate => {
790                    let unregister = request.delegate_request_as_unregister_delegate().unwrap();
791                    let key = DelegateKey::try_decode_fbs(&unregister.key())?;
792                    DelegateRequest::UnregisterDelegate(key)
793                }
794                // An unknown union discriminant is reachable, not `unreachable!()`:
795                // the generated flatbuffers verifier accepts any discriminant it
796                // doesn't recognize (`_ => Ok(())`), and the union type field is a
797                // raw `u8` the (public) TS builder can set to any value. Panicking
798                // here would let a single crafted request take down the connection
799                // handler, so return a per-request error instead.
800                other => {
801                    return Err(crate::client_api::unknown_union_discriminant(
802                        "DelegateRequestType",
803                        other.0,
804                    ));
805                }
806            }
807        };
808
809        Ok(req)
810    }
811}
812
813/// A response to a previous [`ClientRequest`]
814#[derive(Serialize, Deserialize, Debug, Clone)]
815#[non_exhaustive]
816pub enum HostResponse<T = WrappedState> {
817    ContractResponse(#[serde(bound(deserialize = "T: DeserializeOwned"))] ContractResponse<T>),
818    DelegateResponse {
819        key: DelegateKey,
820        values: Vec<OutboundDelegateMsg>,
821    },
822    QueryResponse(QueryResponse),
823    /// A requested action which doesn't require an answer was performed successfully.
824    Ok,
825    /// A chunk of a larger streamed response.
826    StreamChunk {
827        stream_id: u32,
828        index: u32,
829        total: u32,
830        data: Bytes,
831    },
832    /// Header message announcing the start of a streamed response.
833    /// Sent before the corresponding [`StreamChunk`] messages so the client
834    /// can set up incremental consumption via [`WsStreamHandle`].
835    StreamHeader {
836        stream_id: u32,
837        total_bytes: u64,
838        content: StreamContent,
839    },
840}
841
842/// Describes what kind of response is being streamed.
843#[derive(Debug, Serialize, Deserialize, Clone)]
844pub enum StreamContent {
845    /// A streamed GetResponse — the large state is delivered via StreamChunks.
846    GetResponse {
847        key: ContractKey,
848        includes_contract: bool,
849    },
850    /// Raw binary stream (future use).
851    Raw,
852}
853
854type Peer = String;
855
856#[derive(Serialize, Deserialize, Debug, Clone)]
857pub enum QueryResponse {
858    ConnectedPeers { peers: Vec<(Peer, SocketAddr)> },
859    NetworkDebug(NetworkDebugInfo),
860    NodeDiagnostics(NodeDiagnosticsResponse),
861    NeighborHosting(NeighborHostingInfo),
862}
863
864#[derive(Serialize, Deserialize, Debug, Clone)]
865pub struct NetworkDebugInfo {
866    pub subscriptions: Vec<SubscriptionInfo>,
867    pub connected_peers: Vec<(String, SocketAddr)>,
868}
869
870#[derive(Serialize, Deserialize, Debug, Clone)]
871pub struct NodeDiagnosticsResponse {
872    /// Node information
873    pub node_info: Option<NodeInfo>,
874
875    /// Network connectivity information
876    pub network_info: Option<NetworkInfo>,
877
878    /// Contract subscription information
879    pub subscriptions: Vec<SubscriptionInfo>,
880
881    /// Contract states for specific contracts.
882    ///
883    /// Keys are the Base58-encoded contract id (i.e. `ContractKey::Display`),
884    /// matching the convention every other field in this struct uses
885    /// (`peer_id: String`, `connected_peers: Vec<(String, String)>`,
886    /// `ContractHostingEntry::contract_key: String`). Pre-0.7 this was
887    /// `HashMap<ContractKey, ContractState>`, which `serde_json` could not
888    /// serialize because JSON object keys must be strings — every report
889    /// from a node hosting at least one contract had its
890    /// `network_status` silently dropped. See freenet/freenet-core#3987.
891    pub contract_states: std::collections::HashMap<String, ContractState>,
892
893    /// System metrics
894    pub system_metrics: Option<SystemMetrics>,
895
896    /// Information about connected peers with detailed data
897    pub connected_peers_detailed: Vec<ConnectedPeerInfo>,
898}
899
900#[derive(Serialize, Deserialize, Debug, Clone)]
901pub struct NodeInfo {
902    pub peer_id: String,
903    pub is_gateway: bool,
904    pub location: Option<String>,
905    pub listening_address: Option<String>,
906    pub uptime_seconds: u64,
907}
908
909#[derive(Serialize, Deserialize, Debug, Clone)]
910pub struct NetworkInfo {
911    pub connected_peers: Vec<(String, String)>, // (peer_id, address)
912    pub active_connections: usize,
913}
914
915#[derive(Serialize, Deserialize, Debug, Clone)]
916pub struct ContractState {
917    /// Number of nodes subscribed to this contract
918    pub subscribers: u32,
919    /// Peer IDs of nodes that are subscribed to this contract
920    pub subscriber_peer_ids: Vec<String>,
921    /// Size of the contract state in bytes
922    #[serde(default)]
923    pub size_bytes: u64,
924}
925
926#[derive(Serialize, Deserialize, Debug, Clone)]
927pub struct SystemMetrics {
928    pub active_connections: u32,
929    pub hosting_contracts: u32,
930}
931
932#[derive(Serialize, Deserialize, Debug, Clone)]
933pub struct SubscriptionInfo {
934    pub contract_key: ContractInstanceId,
935    pub client_id: usize,
936}
937
938/// Basic information about a connected peer
939#[derive(Serialize, Deserialize, Debug, Clone)]
940pub struct ConnectedPeerInfo {
941    pub peer_id: String,
942    pub address: String,
943}
944
945#[derive(Serialize, Deserialize, Debug, Clone)]
946pub enum NodeQuery {
947    ConnectedPeers,
948    SubscriptionInfo,
949    NodeDiagnostics {
950        /// Diagnostic configuration specifying what information to collect
951        config: NodeDiagnosticsConfig,
952    },
953    /// Query neighbor hosting information for update propagation
954    NeighborHostingInfo,
955}
956
957/// Neighbor hosting information for update propagation
958#[derive(Serialize, Deserialize, Debug, Clone)]
959pub struct NeighborHostingInfo {
960    /// Contracts this node is currently hosting
961    pub my_hosted: Vec<ContractHostingEntry>,
962    /// What we know about neighbor hosting
963    pub neighbor_hosting: Vec<NeighborHostingDetail>,
964    /// Hosting propagation statistics
965    pub stats: HostingStats,
966}
967
968#[derive(Serialize, Deserialize, Debug, Clone)]
969pub struct ContractHostingEntry {
970    /// Full contract key as string
971    pub contract_key: String,
972    /// 32-bit hash for proximity matching
973    pub hosting_hash: u32,
974    /// When this contract was first hosted (Unix timestamp)
975    pub hosted_since: u64,
976}
977
978#[derive(Serialize, Deserialize, Debug, Clone)]
979pub struct NeighborHostingDetail {
980    /// Peer identifier
981    pub peer_id: String,
982    /// Contract hashes this neighbor is known to host
983    pub known_contracts: Vec<u32>,
984    /// Last update received from this neighbor (Unix timestamp)
985    pub last_update: u64,
986    /// Number of updates received from this neighbor
987    pub update_count: u64,
988}
989
990#[derive(Serialize, Deserialize, Debug, Clone)]
991pub struct HostingStats {
992    /// Number of hosting announcements sent
993    pub hosting_announces_sent: u64,
994    /// Number of hosting announcements received
995    pub hosting_announces_received: u64,
996    /// Updates forwarded via proximity (not subscription)
997    pub updates_via_proximity: u64,
998    /// Updates forwarded via subscription
999    pub updates_via_subscription: u64,
1000    /// False positives due to hash collisions
1001    pub false_positive_forwards: u64,
1002    /// Average number of contracts per neighbor
1003    pub avg_neighbor_hosting_size: f32,
1004}
1005
1006#[derive(Serialize, Deserialize, Debug, Clone)]
1007pub struct NodeDiagnosticsConfig {
1008    /// Include basic node information (ID, location, uptime, etc.)
1009    pub include_node_info: bool,
1010
1011    /// Include network connectivity information
1012    pub include_network_info: bool,
1013
1014    /// Include contract subscription information
1015    pub include_subscriptions: bool,
1016
1017    /// Include contract states for specific contracts (empty = all contracts)
1018    pub contract_keys: Vec<ContractKey>,
1019
1020    /// Include memory and performance metrics
1021    pub include_system_metrics: bool,
1022
1023    /// Include detailed information about connected peers (vs basic peer list)
1024    pub include_detailed_peer_info: bool,
1025
1026    /// Include peer IDs of subscribers in contract state information
1027    pub include_subscriber_peer_ids: bool,
1028}
1029
1030impl NodeDiagnosticsConfig {
1031    /// Create a comprehensive diagnostic config for debugging update propagation issues
1032    pub fn for_update_propagation_debugging(contract_key: ContractKey) -> Self {
1033        Self {
1034            include_node_info: true,
1035            include_network_info: true,
1036            include_subscriptions: true,
1037            contract_keys: vec![contract_key],
1038            include_system_metrics: true,
1039            include_detailed_peer_info: true,
1040            include_subscriber_peer_ids: true,
1041        }
1042    }
1043
1044    /// Create a lightweight diagnostic config for basic node status
1045    pub fn basic_status() -> Self {
1046        Self {
1047            include_node_info: true,
1048            include_network_info: true,
1049            include_subscriptions: false,
1050            contract_keys: vec![],
1051            include_system_metrics: false,
1052            include_detailed_peer_info: false,
1053            include_subscriber_peer_ids: false,
1054        }
1055    }
1056
1057    /// Create a full diagnostic config (all information)
1058    pub fn full() -> Self {
1059        Self {
1060            include_node_info: true,
1061            include_network_info: true,
1062            include_subscriptions: true,
1063            contract_keys: vec![], // empty = all contracts
1064            include_system_metrics: true,
1065            include_detailed_peer_info: true,
1066            include_subscriber_peer_ids: true,
1067        }
1068    }
1069}
1070
1071impl HostResponse {
1072    pub fn unwrap_put(self) -> ContractKey {
1073        if let Self::ContractResponse(ContractResponse::PutResponse { key }) = self {
1074            key
1075        } else {
1076            panic!("called `HostResponse::unwrap_put()` on other than `PutResponse` value")
1077        }
1078    }
1079
1080    pub fn unwrap_get(self) -> (WrappedState, Option<ContractContainer>) {
1081        if let Self::ContractResponse(ContractResponse::GetResponse {
1082            contract, state, ..
1083        }) = self
1084        {
1085            (state, contract)
1086        } else {
1087            panic!("called `HostResponse::unwrap_put()` on other than `PutResponse` value")
1088        }
1089    }
1090
1091    pub fn into_fbs_bytes(self) -> Result<Vec<u8>, Box<ClientError>> {
1092        let mut builder = flatbuffers::FlatBufferBuilder::new();
1093        match self {
1094            HostResponse::ContractResponse(res) => match res {
1095                ContractResponse::PutResponse { key } => {
1096                    let instance_data = builder.create_vector(key.as_bytes());
1097                    let instance_offset = FbsContractInstanceId::create(
1098                        &mut builder,
1099                        &ContractInstanceIdArgs {
1100                            data: Some(instance_data),
1101                        },
1102                    );
1103
1104                    let code = Some(builder.create_vector(&key.code_hash().0));
1105                    let key_offset = FbsContractKey::create(
1106                        &mut builder,
1107                        &ContractKeyArgs {
1108                            instance: Some(instance_offset),
1109                            code,
1110                        },
1111                    );
1112
1113                    let put_offset = FbsPutResponse::create(
1114                        &mut builder,
1115                        &PutResponseArgs {
1116                            key: Some(key_offset),
1117                        },
1118                    );
1119
1120                    let contract_response_offset = FbsContractResponse::create(
1121                        &mut builder,
1122                        &ContractResponseArgs {
1123                            contract_response: Some(put_offset.as_union_value()),
1124                            contract_response_type: ContractResponseType::PutResponse,
1125                        },
1126                    );
1127
1128                    let response_offset = FbsHostResponse::create(
1129                        &mut builder,
1130                        &HostResponseArgs {
1131                            response: Some(contract_response_offset.as_union_value()),
1132                            response_type: HostResponseType::ContractResponse,
1133                        },
1134                    );
1135
1136                    finish_host_response_buffer(&mut builder, response_offset);
1137                    Ok(builder.finished_data().to_vec())
1138                }
1139                ContractResponse::UpdateResponse { key, summary } => {
1140                    let instance_data = builder.create_vector(key.as_bytes());
1141                    let instance_offset = FbsContractInstanceId::create(
1142                        &mut builder,
1143                        &ContractInstanceIdArgs {
1144                            data: Some(instance_data),
1145                        },
1146                    );
1147
1148                    let code = Some(builder.create_vector(&key.code_hash().0));
1149
1150                    let key_offset = FbsContractKey::create(
1151                        &mut builder,
1152                        &ContractKeyArgs {
1153                            instance: Some(instance_offset),
1154                            code,
1155                        },
1156                    );
1157
1158                    let summary_data = builder.create_vector(&summary.into_bytes());
1159
1160                    let update_response_offset = FbsUpdateResponse::create(
1161                        &mut builder,
1162                        &UpdateResponseArgs {
1163                            key: Some(key_offset),
1164                            summary: Some(summary_data),
1165                        },
1166                    );
1167
1168                    let contract_response_offset = FbsContractResponse::create(
1169                        &mut builder,
1170                        &ContractResponseArgs {
1171                            contract_response: Some(update_response_offset.as_union_value()),
1172                            contract_response_type: ContractResponseType::UpdateResponse,
1173                        },
1174                    );
1175
1176                    let response_offset = FbsHostResponse::create(
1177                        &mut builder,
1178                        &HostResponseArgs {
1179                            response: Some(contract_response_offset.as_union_value()),
1180                            response_type: HostResponseType::ContractResponse,
1181                        },
1182                    );
1183
1184                    finish_host_response_buffer(&mut builder, response_offset);
1185                    Ok(builder.finished_data().to_vec())
1186                }
1187                ContractResponse::GetResponse {
1188                    key,
1189                    contract: contract_container,
1190                    state,
1191                } => {
1192                    let instance_data = builder.create_vector(key.as_bytes());
1193                    let instance_offset = FbsContractInstanceId::create(
1194                        &mut builder,
1195                        &ContractInstanceIdArgs {
1196                            data: Some(instance_data),
1197                        },
1198                    );
1199
1200                    let code = Some(builder.create_vector(&key.code_hash().0));
1201                    let key_offset = FbsContractKey::create(
1202                        &mut builder,
1203                        &ContractKeyArgs {
1204                            instance: Some(instance_offset),
1205                            code,
1206                        },
1207                    );
1208
1209                    let container_offset = if let Some(contract) = contract_container {
1210                        let data = builder.create_vector(contract.key().as_bytes());
1211
1212                        let instance_offset = FbsContractInstanceId::create(
1213                            &mut builder,
1214                            &ContractInstanceIdArgs { data: Some(data) },
1215                        );
1216
1217                        let code = Some(builder.create_vector(&contract.key().code_hash().0));
1218                        let contract_key_offset = FbsContractKey::create(
1219                            &mut builder,
1220                            &ContractKeyArgs {
1221                                instance: Some(instance_offset),
1222                                code,
1223                            },
1224                        );
1225
1226                        let contract_data =
1227                            builder.create_vector(contract.clone().unwrap_v1().data.data());
1228                        let contract_code_hash =
1229                            builder.create_vector(&contract.clone().unwrap_v1().data.hash().0);
1230
1231                        let contract_code_offset = ContractCode::create(
1232                            &mut builder,
1233                            &ContractCodeArgs {
1234                                data: Some(contract_data),
1235                                code_hash: Some(contract_code_hash),
1236                            },
1237                        );
1238
1239                        let contract_params =
1240                            builder.create_vector(&contract.clone().params().into_bytes());
1241
1242                        let contract_offset = match contract {
1243                            Wasm(V1(..)) => WasmContractV1::create(
1244                                &mut builder,
1245                                &WasmContractV1Args {
1246                                    key: Some(contract_key_offset),
1247                                    data: Some(contract_code_offset),
1248                                    parameters: Some(contract_params),
1249                                },
1250                            ),
1251                        };
1252
1253                        Some(FbsContractContainer::create(
1254                            &mut builder,
1255                            &ContractContainerArgs {
1256                                contract_type: ContractType::WasmContractV1,
1257                                contract: Some(contract_offset.as_union_value()),
1258                            },
1259                        ))
1260                    } else {
1261                        None
1262                    };
1263
1264                    let state_data = builder.create_vector(&state);
1265
1266                    let get_offset = FbsGetResponse::create(
1267                        &mut builder,
1268                        &GetResponseArgs {
1269                            key: Some(key_offset),
1270                            contract: container_offset,
1271                            state: Some(state_data),
1272                        },
1273                    );
1274
1275                    let contract_response_offset = FbsContractResponse::create(
1276                        &mut builder,
1277                        &ContractResponseArgs {
1278                            contract_response_type: ContractResponseType::GetResponse,
1279                            contract_response: Some(get_offset.as_union_value()),
1280                        },
1281                    );
1282
1283                    let response_offset = FbsHostResponse::create(
1284                        &mut builder,
1285                        &HostResponseArgs {
1286                            response: Some(contract_response_offset.as_union_value()),
1287                            response_type: HostResponseType::ContractResponse,
1288                        },
1289                    );
1290
1291                    finish_host_response_buffer(&mut builder, response_offset);
1292                    Ok(builder.finished_data().to_vec())
1293                }
1294                ContractResponse::UpdateNotification { key, update } => {
1295                    let instance_data = builder.create_vector(key.as_bytes());
1296                    let instance_offset = FbsContractInstanceId::create(
1297                        &mut builder,
1298                        &ContractInstanceIdArgs {
1299                            data: Some(instance_data),
1300                        },
1301                    );
1302
1303                    let code = Some(builder.create_vector(&key.code_hash().0));
1304                    let key_offset = FbsContractKey::create(
1305                        &mut builder,
1306                        &ContractKeyArgs {
1307                            instance: Some(instance_offset),
1308                            code,
1309                        },
1310                    );
1311
1312                    let update_data = match update {
1313                        State(state) => {
1314                            let state_data = builder.create_vector(&state.into_bytes());
1315                            let state_update_offset = StateUpdate::create(
1316                                &mut builder,
1317                                &StateUpdateArgs {
1318                                    state: Some(state_data),
1319                                },
1320                            );
1321                            FbsUpdateData::create(
1322                                &mut builder,
1323                                &UpdateDataArgs {
1324                                    update_data_type: UpdateDataType::StateUpdate,
1325                                    update_data: Some(state_update_offset.as_union_value()),
1326                                },
1327                            )
1328                        }
1329                        Delta(delta) => {
1330                            let delta_data = builder.create_vector(&delta.into_bytes());
1331                            let update_offset = DeltaUpdate::create(
1332                                &mut builder,
1333                                &DeltaUpdateArgs {
1334                                    delta: Some(delta_data),
1335                                },
1336                            );
1337                            FbsUpdateData::create(
1338                                &mut builder,
1339                                &UpdateDataArgs {
1340                                    update_data_type: UpdateDataType::DeltaUpdate,
1341                                    update_data: Some(update_offset.as_union_value()),
1342                                },
1343                            )
1344                        }
1345                        StateAndDelta { state, delta } => {
1346                            let state_data = builder.create_vector(&state.into_bytes());
1347                            let delta_data = builder.create_vector(&delta.into_bytes());
1348
1349                            let update_offset = StateAndDeltaUpdate::create(
1350                                &mut builder,
1351                                &StateAndDeltaUpdateArgs {
1352                                    state: Some(state_data),
1353                                    delta: Some(delta_data),
1354                                },
1355                            );
1356
1357                            FbsUpdateData::create(
1358                                &mut builder,
1359                                &UpdateDataArgs {
1360                                    update_data_type: UpdateDataType::StateAndDeltaUpdate,
1361                                    update_data: Some(update_offset.as_union_value()),
1362                                },
1363                            )
1364                        }
1365                        RelatedState { related_to, state } => {
1366                            let state_data = builder.create_vector(&state.into_bytes());
1367                            // RAW 32 bytes, like every other `common.ContractInstanceId`
1368                            // producer. This wrote `related_to.encode()` — base58
1369                            // TEXT — into a field the schema and the TypeScript
1370                            // SDK both read as raw bytes. It is the encode half of
1371                            // the decode bug this change fixes: the same field, the
1372                            // same wrong transformation, mirrored.
1373                            let instance_data = builder.create_vector(related_to.as_bytes());
1374
1375                            let instance_offset = FbsContractInstanceId::create(
1376                                &mut builder,
1377                                &ContractInstanceIdArgs {
1378                                    data: Some(instance_data),
1379                                },
1380                            );
1381
1382                            let update_offset = RelatedStateUpdate::create(
1383                                &mut builder,
1384                                &RelatedStateUpdateArgs {
1385                                    related_to: Some(instance_offset),
1386                                    state: Some(state_data),
1387                                },
1388                            );
1389
1390                            FbsUpdateData::create(
1391                                &mut builder,
1392                                &UpdateDataArgs {
1393                                    update_data_type: UpdateDataType::RelatedStateUpdate,
1394                                    update_data: Some(update_offset.as_union_value()),
1395                                },
1396                            )
1397                        }
1398                        RelatedDelta { related_to, delta } => {
1399                            // RAW 32 bytes, like every other `common.ContractInstanceId`
1400                            // producer. This wrote `related_to.encode()` — base58
1401                            // TEXT — into a field the schema and the TypeScript
1402                            // SDK both read as raw bytes. It is the encode half of
1403                            // the decode bug this change fixes: the same field, the
1404                            // same wrong transformation, mirrored.
1405                            let instance_data = builder.create_vector(related_to.as_bytes());
1406                            let delta_data = builder.create_vector(&delta.into_bytes());
1407
1408                            let instance_offset = FbsContractInstanceId::create(
1409                                &mut builder,
1410                                &ContractInstanceIdArgs {
1411                                    data: Some(instance_data),
1412                                },
1413                            );
1414
1415                            let update_offset = RelatedDeltaUpdate::create(
1416                                &mut builder,
1417                                &RelatedDeltaUpdateArgs {
1418                                    related_to: Some(instance_offset),
1419                                    delta: Some(delta_data),
1420                                },
1421                            );
1422
1423                            FbsUpdateData::create(
1424                                &mut builder,
1425                                &UpdateDataArgs {
1426                                    update_data_type: UpdateDataType::RelatedDeltaUpdate,
1427                                    update_data: Some(update_offset.as_union_value()),
1428                                },
1429                            )
1430                        }
1431                        RelatedStateAndDelta {
1432                            related_to,
1433                            state,
1434                            delta,
1435                        } => {
1436                            // RAW 32 bytes, like every other `common.ContractInstanceId`
1437                            // producer. This wrote `related_to.encode()` — base58
1438                            // TEXT — into a field the schema and the TypeScript
1439                            // SDK both read as raw bytes. It is the encode half of
1440                            // the decode bug this change fixes: the same field, the
1441                            // same wrong transformation, mirrored.
1442                            let instance_data = builder.create_vector(related_to.as_bytes());
1443                            let state_data = builder.create_vector(&state.into_bytes());
1444                            let delta_data = builder.create_vector(&delta.into_bytes());
1445
1446                            let instance_offset = FbsContractInstanceId::create(
1447                                &mut builder,
1448                                &ContractInstanceIdArgs {
1449                                    data: Some(instance_data),
1450                                },
1451                            );
1452
1453                            let update_offset = RelatedStateAndDeltaUpdate::create(
1454                                &mut builder,
1455                                &RelatedStateAndDeltaUpdateArgs {
1456                                    related_to: Some(instance_offset),
1457                                    state: Some(state_data),
1458                                    delta: Some(delta_data),
1459                                },
1460                            );
1461
1462                            FbsUpdateData::create(
1463                                &mut builder,
1464                                &UpdateDataArgs {
1465                                    update_data_type: UpdateDataType::RelatedStateAndDeltaUpdate,
1466                                    update_data: Some(update_offset.as_union_value()),
1467                                },
1468                            )
1469                        }
1470                    };
1471
1472                    let update_notification_offset = FbsUpdateNotification::create(
1473                        &mut builder,
1474                        &UpdateNotificationArgs {
1475                            key: Some(key_offset),
1476                            update: Some(update_data),
1477                        },
1478                    );
1479
1480                    let put_response_offset = FbsContractResponse::create(
1481                        &mut builder,
1482                        &ContractResponseArgs {
1483                            contract_response_type: ContractResponseType::UpdateNotification,
1484                            contract_response: Some(update_notification_offset.as_union_value()),
1485                        },
1486                    );
1487
1488                    let host_response_offset = FbsHostResponse::create(
1489                        &mut builder,
1490                        &HostResponseArgs {
1491                            response_type: HostResponseType::ContractResponse,
1492                            response: Some(put_response_offset.as_union_value()),
1493                        },
1494                    );
1495
1496                    finish_host_response_buffer(&mut builder, host_response_offset);
1497                    Ok(builder.finished_data().to_vec())
1498                }
1499                ContractResponse::SubscribeResponse { key, .. } => {
1500                    // SubscribeResponse FBS type not yet in generated code,
1501                    // serialize as PutResponse (same shape: just a key) so
1502                    // the client receives a valid response instead of a crash.
1503                    let instance_data = builder.create_vector(key.as_bytes());
1504                    let instance_offset = FbsContractInstanceId::create(
1505                        &mut builder,
1506                        &ContractInstanceIdArgs {
1507                            data: Some(instance_data),
1508                        },
1509                    );
1510                    let code = Some(builder.create_vector(&key.code_hash().0));
1511                    let key_offset = FbsContractKey::create(
1512                        &mut builder,
1513                        &ContractKeyArgs {
1514                            instance: Some(instance_offset),
1515                            code,
1516                        },
1517                    );
1518                    let put_offset = FbsPutResponse::create(
1519                        &mut builder,
1520                        &PutResponseArgs {
1521                            key: Some(key_offset),
1522                        },
1523                    );
1524                    let contract_response_offset = FbsContractResponse::create(
1525                        &mut builder,
1526                        &ContractResponseArgs {
1527                            contract_response_type: ContractResponseType::PutResponse,
1528                            contract_response: Some(put_offset.as_union_value()),
1529                        },
1530                    );
1531                    let host_response_offset = FbsHostResponse::create(
1532                        &mut builder,
1533                        &HostResponseArgs {
1534                            response_type: HostResponseType::ContractResponse,
1535                            response: Some(contract_response_offset.as_union_value()),
1536                        },
1537                    );
1538                    finish_host_response_buffer(&mut builder, host_response_offset);
1539                    Ok(builder.finished_data().to_vec())
1540                }
1541                ContractResponse::NotFound { instance_id } => {
1542                    let instance_data = builder.create_vector(instance_id.as_bytes());
1543                    let instance_offset = FbsContractInstanceId::create(
1544                        &mut builder,
1545                        &ContractInstanceIdArgs {
1546                            data: Some(instance_data),
1547                        },
1548                    );
1549
1550                    let not_found_offset = FbsNotFound::create(
1551                        &mut builder,
1552                        &NotFoundArgs {
1553                            instance_id: Some(instance_offset),
1554                        },
1555                    );
1556
1557                    let contract_response_offset = FbsContractResponse::create(
1558                        &mut builder,
1559                        &ContractResponseArgs {
1560                            contract_response_type: ContractResponseType::NotFound,
1561                            contract_response: Some(not_found_offset.as_union_value()),
1562                        },
1563                    );
1564
1565                    let response_offset = FbsHostResponse::create(
1566                        &mut builder,
1567                        &HostResponseArgs {
1568                            response: Some(contract_response_offset.as_union_value()),
1569                            response_type: HostResponseType::ContractResponse,
1570                        },
1571                    );
1572
1573                    finish_host_response_buffer(&mut builder, response_offset);
1574                    Ok(builder.finished_data().to_vec())
1575                }
1576            },
1577            HostResponse::DelegateResponse { key, values } => {
1578                let key_data = builder.create_vector(key.bytes());
1579                let code_hash_data = builder.create_vector(&key.code_hash().0);
1580                let key_offset = FbsDelegateKey::create(
1581                    &mut builder,
1582                    &DelegateKeyArgs {
1583                        key: Some(key_data),
1584                        code_hash: Some(code_hash_data),
1585                    },
1586                );
1587                let mut messages: Vec<WIPOffset<FbsOutboundDelegateMsg>> = Vec::new();
1588                values.iter().for_each(|msg| match msg {
1589                    OutboundDelegateMsg::ApplicationMessage(app) => {
1590                        let payload_data = builder.create_vector(&app.payload);
1591                        let delegate_context_data = builder.create_vector(app.context.as_ref());
1592                        let app_offset = FbsApplicationMessage::create(
1593                            &mut builder,
1594                            &ApplicationMessageArgs {
1595                                payload: Some(payload_data),
1596                                context: Some(delegate_context_data),
1597                                processed: app.processed,
1598                            },
1599                        );
1600                        let msg = FbsOutboundDelegateMsg::create(
1601                            &mut builder,
1602                            &OutboundDelegateMsgArgs {
1603                                inbound_type: OutboundDelegateMsgType::common_ApplicationMessage,
1604                                inbound: Some(app_offset.as_union_value()),
1605                            },
1606                        );
1607                        messages.push(msg);
1608                    }
1609                    OutboundDelegateMsg::RequestUserInput(input) => {
1610                        let message_data = builder.create_vector(input.message.bytes());
1611                        let mut responses: Vec<WIPOffset<FbsClientResponse>> = Vec::new();
1612                        input.responses.iter().for_each(|resp| {
1613                            let response_data = builder.create_vector(resp.bytes());
1614                            let response = FbsClientResponse::create(
1615                                &mut builder,
1616                                &ClientResponseArgs {
1617                                    data: Some(response_data),
1618                                },
1619                            );
1620                            responses.push(response)
1621                        });
1622                        let responses_offset = builder.create_vector(&responses);
1623                        let input_offset = FbsRequestUserInput::create(
1624                            &mut builder,
1625                            &RequestUserInputArgs {
1626                                request_id: input.request_id,
1627                                message: Some(message_data),
1628                                responses: Some(responses_offset),
1629                            },
1630                        );
1631                        let msg = FbsOutboundDelegateMsg::create(
1632                            &mut builder,
1633                            &OutboundDelegateMsgArgs {
1634                                inbound_type: OutboundDelegateMsgType::RequestUserInput,
1635                                inbound: Some(input_offset.as_union_value()),
1636                            },
1637                        );
1638                        messages.push(msg);
1639                    }
1640                    OutboundDelegateMsg::ContextUpdated(context) => {
1641                        let context_data = builder.create_vector(context.as_ref());
1642                        let context_offset = FbsContextUpdated::create(
1643                            &mut builder,
1644                            &ContextUpdatedArgs {
1645                                context: Some(context_data),
1646                            },
1647                        );
1648                        let msg = FbsOutboundDelegateMsg::create(
1649                            &mut builder,
1650                            &OutboundDelegateMsgArgs {
1651                                inbound_type: OutboundDelegateMsgType::ContextUpdated,
1652                                inbound: Some(context_offset.as_union_value()),
1653                            },
1654                        );
1655                        messages.push(msg);
1656                    }
1657                    OutboundDelegateMsg::GetContractRequest(_) => {
1658                        // GetContractRequest should be handled by the executor and never
1659                        // reach client serialization. If we get here, it's a bug.
1660                        tracing::error!(
1661                            "GetContractRequest reached client serialization - this is a bug"
1662                        );
1663                    }
1664                    OutboundDelegateMsg::PutContractRequest(_) => {
1665                        // PutContractRequest should be handled by the executor and never
1666                        // reach client serialization. If we get here, it's a bug.
1667                        tracing::error!(
1668                            "PutContractRequest reached client serialization - this is a bug"
1669                        );
1670                    }
1671                    OutboundDelegateMsg::UpdateContractRequest(_) => {
1672                        tracing::error!(
1673                            "UpdateContractRequest reached client serialization - this is a bug"
1674                        );
1675                    }
1676                    OutboundDelegateMsg::SubscribeContractRequest(_) => {
1677                        tracing::error!(
1678                            "SubscribeContractRequest reached client serialization - this is a bug"
1679                        );
1680                    }
1681                    OutboundDelegateMsg::SendDelegateMessage(_) => {
1682                        tracing::error!(
1683                            "SendDelegateMessage reached client serialization - this is a bug"
1684                        );
1685                    }
1686                });
1687                let messages_offset = builder.create_vector(&messages);
1688                let delegate_response_offset = FbsDelegateResponse::create(
1689                    &mut builder,
1690                    &DelegateResponseArgs {
1691                        key: Some(key_offset),
1692                        values: Some(messages_offset),
1693                    },
1694                );
1695                let host_response_offset = FbsHostResponse::create(
1696                    &mut builder,
1697                    &HostResponseArgs {
1698                        response_type: HostResponseType::DelegateResponse,
1699                        response: Some(delegate_response_offset.as_union_value()),
1700                    },
1701                );
1702                finish_host_response_buffer(&mut builder, host_response_offset);
1703                Ok(builder.finished_data().to_vec())
1704            }
1705            HostResponse::Ok => {
1706                let ok_offset = FbsOk::create(&mut builder, &OkArgs { msg: None });
1707                let host_response_offset = FbsHostResponse::create(
1708                    &mut builder,
1709                    &HostResponseArgs {
1710                        response_type: HostResponseType::Ok,
1711                        response: Some(ok_offset.as_union_value()),
1712                    },
1713                );
1714                finish_host_response_buffer(&mut builder, host_response_offset);
1715                Ok(builder.finished_data().to_vec())
1716            }
1717            HostResponse::QueryResponse(_) => unimplemented!(),
1718            HostResponse::StreamChunk {
1719                stream_id,
1720                index,
1721                total,
1722                data,
1723            } => {
1724                let data_offset = builder.create_vector(&data);
1725                let chunk_offset = FbsHostStreamChunk::create(
1726                    &mut builder,
1727                    &FbsHostStreamChunkArgs {
1728                        stream_id,
1729                        index,
1730                        total,
1731                        data: Some(data_offset),
1732                    },
1733                );
1734                let host_response_offset = FbsHostResponse::create(
1735                    &mut builder,
1736                    &HostResponseArgs {
1737                        response_type: HostResponseType::StreamChunk,
1738                        response: Some(chunk_offset.as_union_value()),
1739                    },
1740                );
1741                finish_host_response_buffer(&mut builder, host_response_offset);
1742                Ok(builder.finished_data().to_vec())
1743            }
1744            HostResponse::StreamHeader { .. } => {
1745                // StreamHeader is only sent over bincode (Native encoding) to
1746                // streaming-capable clients. Flatbuffers clients use transparent
1747                // reassembly via StreamChunk only.
1748                Err(Box::new(ClientError::from(ErrorKind::Unhandled {
1749                    cause: "StreamHeader is not supported over flatbuffers encoding".into(),
1750                })))
1751            }
1752        }
1753    }
1754}
1755
1756impl Display for HostResponse {
1757    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1758        match self {
1759            HostResponse::ContractResponse(res) => match res {
1760                ContractResponse::PutResponse { key } => {
1761                    f.write_fmt(format_args!("put response for `{key}`"))
1762                }
1763                ContractResponse::UpdateResponse { key, .. } => {
1764                    f.write_fmt(format_args!("update response for `{key}`"))
1765                }
1766                ContractResponse::GetResponse { key, .. } => {
1767                    f.write_fmt(format_args!("get response for `{key}`"))
1768                }
1769                ContractResponse::UpdateNotification { key, .. } => {
1770                    f.write_fmt(format_args!("update notification for `{key}`"))
1771                }
1772                ContractResponse::SubscribeResponse { key, .. } => {
1773                    f.write_fmt(format_args!("subscribe response for `{key}`"))
1774                }
1775                ContractResponse::NotFound { instance_id } => {
1776                    f.write_fmt(format_args!("not found for `{instance_id}`"))
1777                }
1778            },
1779            HostResponse::DelegateResponse { .. } => write!(f, "delegate responses"),
1780            HostResponse::Ok => write!(f, "ok response"),
1781            HostResponse::QueryResponse(_) => write!(f, "query response"),
1782            HostResponse::StreamChunk {
1783                stream_id,
1784                index,
1785                total,
1786                ..
1787            } => write!(f, "stream chunk {index}/{total} (stream {stream_id})"),
1788            HostResponse::StreamHeader {
1789                stream_id,
1790                total_bytes,
1791                ..
1792            } => write!(f, "stream header (stream {stream_id}, {total_bytes} bytes)"),
1793        }
1794    }
1795}
1796
1797#[derive(Clone, Serialize, Deserialize, Debug)]
1798#[non_exhaustive]
1799pub enum ContractResponse<T = WrappedState> {
1800    GetResponse {
1801        key: ContractKey,
1802        contract: Option<ContractContainer>,
1803        #[serde(bound(deserialize = "T: DeserializeOwned"))]
1804        state: T,
1805    },
1806    PutResponse {
1807        key: ContractKey,
1808    },
1809    /// Message sent when there is an update to a subscribed contract.
1810    UpdateNotification {
1811        key: ContractKey,
1812        #[serde(deserialize_with = "UpdateData::deser_update_data")]
1813        update: UpdateData<'static>,
1814    },
1815    /// Successful update
1816    UpdateResponse {
1817        key: ContractKey,
1818        #[serde(deserialize_with = "StateSummary::deser_state_summary")]
1819        summary: StateSummary<'static>,
1820    },
1821    SubscribeResponse {
1822        key: ContractKey,
1823        subscribed: bool,
1824    },
1825    /// Contract was not found after exhaustive search.
1826    /// This is an explicit response that distinguishes "contract doesn't exist"
1827    /// from other failure modes like timeouts or network errors.
1828    NotFound {
1829        /// The instance ID that was searched for.
1830        instance_id: ContractInstanceId,
1831    },
1832}
1833
1834impl<T> From<ContractResponse<T>> for HostResponse<T> {
1835    fn from(value: ContractResponse<T>) -> HostResponse<T> {
1836        HostResponse::ContractResponse(value)
1837    }
1838}
1839
1840#[cfg(test)]
1841mod node_diagnostics_response_tests {
1842    use super::{
1843        ConnectedPeerInfo, ContractState, NetworkInfo, NodeDiagnosticsResponse, NodeInfo,
1844        SubscriptionInfo, SystemMetrics,
1845    };
1846    use crate::contract_interface::ContractInstanceId;
1847    use std::collections::HashMap;
1848
1849    /// Regression for freenet/freenet-core#3987.
1850    ///
1851    /// Pre-0.7 `contract_states` was `HashMap<ContractKey, ContractState>`.
1852    /// `ContractKey` derives `Serialize` as a struct (`{instance, code}`),
1853    /// which `serde_json` rejects with `key must be a string` because JSON
1854    /// object keys must be strings. The wire path between core and clients
1855    /// is bincode (which doesn't care about key types), so the bug stayed
1856    /// invisible until the `freenet service report` binary tried to
1857    /// JSON-serialize the response for upload — every report from a node
1858    /// hosting at least one contract uploaded with empty `network_status`.
1859    ///
1860    /// All six fields are populated so that any future `pub` field added
1861    /// to the struct gets exercised by serde_json the moment a contributor
1862    /// sets a non-default value here. If a future field reintroduces the
1863    /// non-string-key pattern (e.g. `HashMap<PeerId, _>`), this test will
1864    /// fail at the source instead of silently breaking the report path.
1865    #[test]
1866    fn node_diagnostics_response_json_round_trips() {
1867        let mut contract_states = HashMap::new();
1868        contract_states.insert(
1869            "6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9".to_string(),
1870            ContractState {
1871                subscribers: 3,
1872                subscriber_peer_ids: vec!["peer-a".to_string(), "peer-b".to_string()],
1873                size_bytes: 1024,
1874            },
1875        );
1876
1877        let response = NodeDiagnosticsResponse {
1878            node_info: Some(NodeInfo {
1879                peer_id: "peer-self".to_string(),
1880                is_gateway: true,
1881                location: Some("0.5".to_string()),
1882                listening_address: Some("0.0.0.0:31337".to_string()),
1883                uptime_seconds: 3600,
1884            }),
1885            network_info: Some(NetworkInfo {
1886                connected_peers: vec![("peer-x".to_string(), "10.0.0.1:31337".to_string())],
1887                active_connections: 1,
1888            }),
1889            subscriptions: vec![SubscriptionInfo {
1890                contract_key: ContractInstanceId::new([7u8; 32]),
1891                client_id: 42,
1892            }],
1893            contract_states,
1894            system_metrics: Some(SystemMetrics {
1895                active_connections: 1,
1896                hosting_contracts: 1,
1897            }),
1898            connected_peers_detailed: vec![ConnectedPeerInfo {
1899                peer_id: "peer-x".to_string(),
1900                address: "10.0.0.1:31337".to_string(),
1901            }],
1902        };
1903
1904        let json = serde_json::to_string(&response).expect("must serialize to JSON");
1905        let parsed: serde_json::Value = serde_json::from_str(&json).expect("output is valid JSON");
1906
1907        // Every top-level field is present and distinguishable.
1908        let obj = parsed.as_object().expect("top-level must be object");
1909        assert_eq!(obj.len(), 6, "expected six top-level fields, got {obj:?}");
1910        assert_eq!(parsed["node_info"]["peer_id"], "peer-self");
1911        assert_eq!(parsed["network_info"]["active_connections"], 1);
1912        assert_eq!(parsed["subscriptions"][0]["client_id"], 42);
1913        assert_eq!(parsed["system_metrics"]["hosting_contracts"], 1);
1914        assert_eq!(parsed["connected_peers_detailed"][0]["peer_id"], "peer-x");
1915
1916        let states = parsed["contract_states"]
1917            .as_object()
1918            .expect("contract_states must be a JSON object");
1919        assert_eq!(states.len(), 1);
1920        assert_eq!(
1921            states["6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9"]["subscribers"],
1922            3
1923        );
1924
1925        // Bincode round-trip must also still work for the same value
1926        // (the new wire format is the contract; older clients are
1927        // documented as incompatible in CHANGELOG).
1928        let bytes = bincode::serialize(&response).expect("bincode must serialize");
1929        let decoded: NodeDiagnosticsResponse =
1930            bincode::deserialize(&bytes).expect("bincode must round-trip");
1931        assert_eq!(
1932            decoded.contract_states.len(),
1933            1,
1934            "bincode round-trip preserves contract_states entries"
1935        );
1936    }
1937}
1938
1939#[cfg(test)]
1940mod client_request_test {
1941    use crate::client_api::{ContractRequest, TryFromFbs, WsApiError};
1942    use crate::contract_interface::UpdateData;
1943    use crate::generated::client_request::root_as_client_request;
1944
1945    const EXPECTED_ENCODED_CONTRACT_ID: &str = "6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9";
1946
1947    #[test]
1948    fn test_build_contract_put_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
1949        let put_req_op = vec![
1950            4, 0, 0, 0, 244, 255, 255, 255, 16, 0, 0, 0, 0, 0, 0, 1, 8, 0, 12, 0, 11, 0, 4, 0, 8,
1951            0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 1, 198, 255, 255, 255, 12, 0, 0, 0, 20, 0, 0, 0, 36, 0,
1952            0, 0, 170, 255, 255, 255, 4, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8,
1953            8, 0, 10, 0, 9, 0, 4, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 1, 10, 0, 16, 0, 12, 0, 8, 0, 4,
1954            0, 10, 0, 0, 0, 12, 0, 0, 0, 76, 0, 0, 0, 92, 0, 0, 0, 176, 255, 255, 255, 8, 0, 0, 0,
1955            16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0,
1956            85, 111, 11, 171, 40, 85, 240, 177, 207, 81, 106, 157, 173, 90, 234, 2, 250, 253, 75,
1957            210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17, 108, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6,
1958            7, 8, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 8, 0, 0, 0, 1, 2,
1959            3, 4, 5, 6, 7, 8, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8,
1960        ];
1961        let request = if let Ok(client_request) = root_as_client_request(&put_req_op) {
1962            let contract_request = client_request.client_request_as_contract_request().unwrap();
1963            ContractRequest::try_decode_fbs(&contract_request)?
1964        } else {
1965            panic!("failed to decode client request")
1966        };
1967
1968        match request {
1969            ContractRequest::Put {
1970                contract,
1971                state,
1972                related_contracts: _,
1973                subscribe,
1974                blocking_subscribe,
1975            } => {
1976                assert_eq!(
1977                    contract.to_string(),
1978                    "WasmContainer([api=0.0.1](D8fdVLbRyMLw5mZtPRpWMFcrXGN2z8Nq8UGcLGPFBg2W))"
1979                );
1980                assert_eq!(contract.unwrap_v1().data.data(), &[1, 2, 3, 4, 5, 6, 7, 8]);
1981                assert_eq!(state.to_vec(), &[1, 2, 3, 4, 5, 6, 7, 8]);
1982                assert!(!subscribe);
1983                assert!(!blocking_subscribe);
1984            }
1985            _ => panic!("wrong contract request type"),
1986        }
1987
1988        Ok(())
1989    }
1990
1991    #[test]
1992    fn test_build_contract_get_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
1993        let get_req_op = vec![
1994            4, 0, 0, 0, 244, 255, 255, 255, 16, 0, 0, 0, 0, 0, 0, 1, 8, 0, 12, 0, 11, 0, 4, 0, 8,
1995            0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 3, 222, 255, 255, 255, 12, 0, 0, 0, 8, 0, 12, 0, 8, 0, 4,
1996            0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0,
1997            4, 0, 0, 0, 32, 0, 0, 0, 85, 111, 11, 171, 40, 85, 240, 177, 207, 81, 106, 157, 173,
1998            90, 234, 2, 250, 253, 75, 210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17, 108,
1999        ];
2000        let request = if let Ok(client_request) = root_as_client_request(&get_req_op) {
2001            let contract_request = client_request.client_request_as_contract_request().unwrap();
2002            ContractRequest::try_decode_fbs(&contract_request)?
2003        } else {
2004            panic!("failed to decode client request")
2005        };
2006
2007        match request {
2008            ContractRequest::Get {
2009                key,
2010                return_contract_code: fetch_contract,
2011                subscribe,
2012                blocking_subscribe,
2013            } => {
2014                assert_eq!(key.encode(), EXPECTED_ENCODED_CONTRACT_ID);
2015                assert!(!fetch_contract);
2016                assert!(!subscribe);
2017                assert!(!blocking_subscribe);
2018            }
2019            _ => panic!("wrong contract request type"),
2020        }
2021
2022        Ok(())
2023    }
2024
2025    /// A well-formed FlatBuffers `UpdateRequest` decodes to the expected key
2026    /// and delta.
2027    ///
2028    /// Built programmatically rather than from a hardcoded byte blob. The blob
2029    /// this replaces carried a present-but-ZERO-LENGTH `code` field, which is
2030    /// what the TypeScript SDK's `ContractKey.fromInstanceId(...)` emits: a
2031    /// request the node cannot serve, because an UPDATE supplies no contract
2032    /// code and freenet-core resolves the WASM by code hash. Nobody could see
2033    /// that from reading the test; it took instrumenting the decoder. So the
2034    /// fixture now carries a real 32-byte hash and says so in source.
2035    ///
2036    /// The empty and absent cases are covered in
2037    /// `contract_interface::key::fbs_tests`, next to the decoder that rejects
2038    /// them.
2039    #[test]
2040    fn test_build_contract_update_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
2041        use crate::generated::client_request::{
2042            finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
2043            ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
2044            ContractRequestType, Update as FbsUpdate, UpdateArgs,
2045        };
2046        use crate::generated::common::{
2047            ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
2048            ContractKey as FbsContractKey, ContractKeyArgs, DeltaUpdate, DeltaUpdateArgs,
2049            UpdateData as FbsUpdateData, UpdateDataArgs, UpdateDataType,
2050        };
2051        use crate::prelude::ContractInstanceId;
2052
2053        // Derive the instance bytes from the expected id rather than pasting a
2054        // magic array, so the assertion below can't drift from the fixture.
2055        let instance_id = ContractInstanceId::try_from(EXPECTED_ENCODED_CONTRACT_ID.to_string())?;
2056        // A distinct, arbitrary code hash: if the decoder ever re-hashes it
2057        // (the BLAKE3(BLAKE3(wasm)) bug this PR fixes), the assertion fails.
2058        let code_hash = [42u8; 32];
2059        let delta_bytes = [1u8, 2, 3, 4, 5, 6, 7, 8];
2060
2061        let mut b = flatbuffers::FlatBufferBuilder::new();
2062
2063        let instance_data = b.create_vector(instance_id.as_bytes());
2064        let instance_offset = FbsContractInstanceId::create(
2065            &mut b,
2066            &ContractInstanceIdArgs {
2067                data: Some(instance_data),
2068            },
2069        );
2070        let code = Some(b.create_vector(&code_hash));
2071        let key_offset = FbsContractKey::create(
2072            &mut b,
2073            &ContractKeyArgs {
2074                instance: Some(instance_offset),
2075                code,
2076            },
2077        );
2078
2079        let delta = b.create_vector(&delta_bytes);
2080        let delta_offset = DeltaUpdate::create(&mut b, &DeltaUpdateArgs { delta: Some(delta) });
2081        let update_data_offset = FbsUpdateData::create(
2082            &mut b,
2083            &UpdateDataArgs {
2084                update_data_type: UpdateDataType::DeltaUpdate,
2085                update_data: Some(delta_offset.as_union_value()),
2086            },
2087        );
2088
2089        let update_offset = FbsUpdate::create(
2090            &mut b,
2091            &UpdateArgs {
2092                key: Some(key_offset),
2093                data: Some(update_data_offset),
2094            },
2095        );
2096        let contract_offset = FbsContractRequest::create(
2097            &mut b,
2098            &ContractRequestArgs {
2099                contract_request_type: ContractRequestType::Update,
2100                contract_request: Some(update_offset.as_union_value()),
2101            },
2102        );
2103        let client_offset = FbsClientRequest::create(
2104            &mut b,
2105            &ClientRequestArgs {
2106                client_request_type: ClientRequestType::ContractRequest,
2107                client_request: Some(contract_offset.as_union_value()),
2108            },
2109        );
2110        finish_client_request_buffer(&mut b, client_offset);
2111
2112        let update_op = b.finished_data().to_vec();
2113        let request = if let Ok(client_request) = root_as_client_request(&update_op) {
2114            let contract_request = client_request.client_request_as_contract_request().unwrap();
2115            ContractRequest::try_decode_fbs(&contract_request)?
2116        } else {
2117            panic!("failed to decode client request")
2118        };
2119
2120        match request {
2121            ContractRequest::Update { key, data } => {
2122                assert_eq!(key.encoded_contract_id(), EXPECTED_ENCODED_CONTRACT_ID);
2123                assert_eq!(
2124                    key.code_hash().as_ref(),
2125                    &code_hash,
2126                    "the code hash must survive decode unchanged, not be re-hashed"
2127                );
2128                match data {
2129                    UpdateData::Delta(delta) => {
2130                        assert_eq!(delta.to_vec(), &delta_bytes)
2131                    }
2132                    _ => panic!("wrong update data type"),
2133                }
2134            }
2135            _ => panic!("wrong contract request type"),
2136        }
2137
2138        Ok(())
2139    }
2140
2141    /// The exact bytes the TypeScript SDK's own test suite asserts as a correct
2142    /// `UpdateRequest`: pinned here so the two suites cannot disagree silently.
2143    ///
2144    /// Copied verbatim from `typescript/tests/websocket-interface.test.ts`
2145    /// (`EXPECTED_UPDATE_REQ`). It is byte-for-byte the blob this PR deleted
2146    /// from the Rust side, which is exactly how the original double-hash bug
2147    /// survived: both suites pinned the same bytes in mirror, and neither ever
2148    /// crossed the language boundary, so a shape that no Rust decoder accepted
2149    /// stayed "verified" on the TypeScript side.
2150    ///
2151    /// Its `code` field is present and ZERO-LENGTH, which is what
2152    /// `ContractKey.fromInstanceId(...)` emits. After this change the Rust
2153    /// decoder hard-errors on it, while `npm test` still asserts it is correct
2154    /// and stays green. Both suites run in CI, so this test is the only thing
2155    /// that makes that disagreement visible.
2156    ///
2157    /// Scope, so this is not over-trusted: it is a COPY of the TypeScript
2158    /// array, not a reference to it. Nothing mechanically ties the two, so
2159    /// editing the TypeScript side fails nothing here. (`include_str!` across
2160    /// to `typescript/` is not the fix: `rust/` is the published package root
2161    /// and the path would escape it.) So this is a convention, and the
2162    /// convention is: if the TypeScript SDK is fixed (see
2163    /// freenet/freenet-core#4978, whose candidate is making `UpdateRequest`
2164    /// reject a key whose `code` is not 32 bytes), update the array and this
2165    /// constant together. Leaving one behind re-creates the mirror-pinning
2166    /// that hid the original bug.
2167    const TS_SDK_EXPECTED_UPDATE_REQ: &[u8] = &[
2168        4, 0, 0, 0, 220, 255, 255, 255, 8, 0, 0, 0, 0, 0, 0, 1, 232, 255, 255, 255, 8, 0, 0, 0, 0,
2169        0, 0, 2, 204, 255, 255, 255, 16, 0, 0, 0, 52, 0, 0, 0, 8, 0, 12, 0, 11, 0, 4, 0, 8, 0, 0,
2170        0, 8, 0, 0, 0, 0, 0, 0, 2, 210, 255, 255, 255, 4, 0, 0, 0, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7,
2171        8, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 8,
2172        0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0, 85, 111, 11, 171, 40, 85, 240, 177, 207, 81,
2173        106, 157, 173, 90, 234, 2, 250, 253, 75, 210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17,
2174        108,
2175    ];
2176
2177    /// The cross-language contract: what the TypeScript SDK emits for an
2178    /// instance-id-only UPDATE is rejected here, with the actionable message.
2179    ///
2180    /// Asserting the MESSAGE and not just the rejection is the point. The
2181    /// TypeScript developer whose request this is gets exactly this string
2182    /// back over the WebSocket, and it is the only thing telling them the key
2183    /// needs both parts. A revert to the bare `try_from` error would leave the
2184    /// rejection green while restoring `"invalid data"`.
2185    #[test]
2186    fn typescript_sdk_instance_only_update_is_rejected_with_guidance() {
2187        let client_request = root_as_client_request(TS_SDK_EXPECTED_UPDATE_REQ)
2188            .expect("the TS SDK blob must still be a well-formed ClientRequest");
2189        let contract_request = client_request
2190            .client_request_as_contract_request()
2191            .expect("the TS SDK blob must still be a ContractRequest");
2192
2193        let err = ContractRequest::try_decode_fbs(&contract_request)
2194            .expect_err("an instance-id-only UPDATE must be rejected");
2195        let msg = err.to_string();
2196
2197        assert!(
2198            msg.contains("ContractKey.code") && msg.contains("got 0 bytes"),
2199            "the TS SDK's zero-length code must be named explicitly, got: {msg}"
2200        );
2201        assert!(
2202            msg.contains("new ContractKey(instance, code)"),
2203            "the error must tell a TypeScript developer how to build the key, got: {msg}"
2204        );
2205        assert!(
2206            msg.contains("4978"),
2207            "the error must point at the tracking issue for the real fix, got: {msg}"
2208        );
2209    }
2210
2211    /// Build a `ClientRequest` carrying a GET or SUBSCRIBE whose `ContractKey`
2212    /// has an `instance` vector of the given length.
2213    ///
2214    /// The length is a parameter because the flatbuffers verifier checks that a
2215    /// `(required)` vector is PRESENT, not that it is the right SIZE, so a
2216    /// wrong-length instance is a shape a peer can actually put on the wire and
2217    /// that `flatbuffers::root` will happily hand to the decoder.
2218    fn client_request_with_instance_len(instance_len: usize, subscribe: bool) -> Vec<u8> {
2219        use crate::generated::client_request::{
2220            finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
2221            ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
2222            ContractRequestType, Get as FbsGet, GetArgs, Subscribe as FbsSubscribe, SubscribeArgs,
2223        };
2224        use crate::generated::common::{
2225            ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
2226            ContractKey as FbsContractKey, ContractKeyArgs,
2227        };
2228
2229        let mut b = flatbuffers::FlatBufferBuilder::new();
2230        let instance_data = b.create_vector(&vec![1u8; instance_len]);
2231        let instance_offset = FbsContractInstanceId::create(
2232            &mut b,
2233            &ContractInstanceIdArgs {
2234                data: Some(instance_data),
2235            },
2236        );
2237        let code = Some(b.create_vector(&[42u8; 32]));
2238        let key_offset = FbsContractKey::create(
2239            &mut b,
2240            &ContractKeyArgs {
2241                instance: Some(instance_offset),
2242                code,
2243            },
2244        );
2245
2246        let (request_type, request_offset) = if subscribe {
2247            let sub = FbsSubscribe::create(
2248                &mut b,
2249                &SubscribeArgs {
2250                    key: Some(key_offset),
2251                    summary: None,
2252                },
2253            );
2254            (ContractRequestType::Subscribe, sub.as_union_value())
2255        } else {
2256            let get = FbsGet::create(
2257                &mut b,
2258                &GetArgs {
2259                    key: Some(key_offset),
2260                    fetch_contract: false,
2261                    subscribe: false,
2262                    blocking_subscribe: false,
2263                },
2264            );
2265            (ContractRequestType::Get, get.as_union_value())
2266        };
2267
2268        let contract_offset = FbsContractRequest::create(
2269            &mut b,
2270            &ContractRequestArgs {
2271                contract_request_type: request_type,
2272                contract_request: Some(request_offset),
2273            },
2274        );
2275        let client_offset = FbsClientRequest::create(
2276            &mut b,
2277            &ClientRequestArgs {
2278                client_request_type: ClientRequestType::ContractRequest,
2279                client_request: Some(contract_offset.as_union_value()),
2280            },
2281        );
2282        finish_client_request_buffer(&mut b, client_offset);
2283        b.finished_data().to_vec()
2284    }
2285
2286    fn decode_client_request(bytes: &[u8]) -> Result<ContractRequest<'_>, WsApiError> {
2287        let client_request =
2288            root_as_client_request(bytes).expect("must be a well-formed ClientRequest");
2289        let contract_request = client_request
2290            .client_request_as_contract_request()
2291            .expect("must be a ContractRequest");
2292        ContractRequest::try_decode_fbs(&contract_request)
2293    }
2294
2295    /// A GET whose instance is the wrong length is rejected, not panicked on.
2296    ///
2297    /// Pinned at the REQUEST level rather than through `ContractKey`'s decoder,
2298    /// because GET does not go through `ContractKey::try_decode_fbs` at all: it
2299    /// reads the instance bytes directly. A test that only covers the UPDATE
2300    /// path leaves a revert of this site green, which is exactly the state this
2301    /// suite was in before: the panic fix reached three call sites and only one
2302    /// of them was pinned.
2303    #[test]
2304    fn get_with_wrong_length_instance_is_rejected_not_panicking() {
2305        let bytes = client_request_with_instance_len(8, false);
2306        let short = decode_client_request(&bytes)
2307            .expect_err("a GET with an 8-byte instance must be rejected");
2308        assert!(
2309            short.to_string().contains("ContractKey.instance")
2310                && short.to_string().contains("got 8 bytes"),
2311            "got: {short}"
2312        );
2313
2314        let bytes = client_request_with_instance_len(64, false);
2315        let long = decode_client_request(&bytes)
2316            .expect_err("a GET with a 64-byte instance must be rejected");
2317        assert!(long.to_string().contains("got 64 bytes"), "got: {long}");
2318    }
2319
2320    /// Same for SUBSCRIBE, which is a third independent decode site.
2321    #[test]
2322    fn subscribe_with_wrong_length_instance_is_rejected_not_panicking() {
2323        let bytes = client_request_with_instance_len(8, true);
2324        let short = decode_client_request(&bytes)
2325            .expect_err("a SUBSCRIBE with an 8-byte instance must be rejected");
2326        assert!(
2327            short.to_string().contains("ContractKey.instance")
2328                && short.to_string().contains("got 8 bytes"),
2329            "got: {short}"
2330        );
2331
2332        let bytes = client_request_with_instance_len(64, true);
2333        let long = decode_client_request(&bytes)
2334            .expect_err("a SUBSCRIBE with a 64-byte instance must be rejected");
2335        assert!(long.to_string().contains("got 64 bytes"), "got: {long}");
2336    }
2337
2338    /// A well-formed GET still decodes: the length guard must reject the wrong
2339    /// size without breaking the right one.
2340    #[test]
2341    fn get_with_valid_instance_still_decodes() {
2342        let bytes = client_request_with_instance_len(32, false);
2343        let req = decode_client_request(&bytes).expect("a 32-byte instance must still decode");
2344        assert!(
2345            matches!(req, ContractRequest::Get { .. }),
2346            "expected a Get, got {req:?}"
2347        );
2348    }
2349
2350    /// The flatbuffers decode path must NOT panic on an unknown
2351    /// `ContractRequestType` discriminant. Same class as the `DelegateRequest`
2352    /// guard: the generated union verifier accepts any discriminant it doesn't
2353    /// recognize (`_ => Ok(())`), and the union type field is a raw `u8` a
2354    /// client can set to any value. Before the fix this hit `unreachable!()`
2355    /// and downed the connection handler; now it is a clean per-request error.
2356    #[test]
2357    fn fbs_decode_rejects_unknown_contract_discriminant() {
2358        use crate::generated::client_request::{
2359            finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
2360            ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
2361            ContractRequestType, DelegateKey as FbsDelegateKey, DelegateKeyArgs,
2362            UnregisterDelegate, UnregisterDelegateArgs,
2363        };
2364
2365        let mut b = flatbuffers::FlatBufferBuilder::new();
2366        // Any well-formed table satisfies the required union value; the
2367        // discriminant is unknown so the verifier never inspects it.
2368        let key = b.create_vector(&[0u8; 32]);
2369        let code_hash = b.create_vector(&[0u8; 32]);
2370        let dk = FbsDelegateKey::create(
2371            &mut b,
2372            &DelegateKeyArgs {
2373                key: Some(key),
2374                code_hash: Some(code_hash),
2375            },
2376        );
2377        let dummy = UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
2378        // 99 is past the max known ContractRequestType (Subscribe = 4).
2379        let contract = FbsContractRequest::create(
2380            &mut b,
2381            &ContractRequestArgs {
2382                contract_request_type: ContractRequestType(99),
2383                contract_request: Some(dummy.as_union_value()),
2384            },
2385        );
2386        let client = FbsClientRequest::create(
2387            &mut b,
2388            &ClientRequestArgs {
2389                client_request_type: ClientRequestType::ContractRequest,
2390                client_request: Some(contract.as_union_value()),
2391            },
2392        );
2393        finish_client_request_buffer(&mut b, client);
2394        let bytes = b.finished_data().to_vec();
2395
2396        let client =
2397            root_as_client_request(&bytes).expect("verifier accepts an unknown union discriminant");
2398        let fbs_contract = client
2399            .client_request_as_contract_request()
2400            .expect("client_request is a ContractRequest");
2401        assert!(
2402            ContractRequest::try_decode_fbs(&fbs_contract).is_err(),
2403            "an unknown ContractRequestType discriminant must be a clean \
2404             per-request error, never a panic that downs the connection handler"
2405        );
2406    }
2407}
2408
2409/// Wire-format pins for [`DelegateRequest`].
2410///
2411/// `DelegateRequest` crosses the client<->node boundary as bincode (the
2412/// `EncodingProtocol::Native` path in freenet-core; the Rust clients in this
2413/// crate — `browser.rs` and `regular.rs` — both `bincode::serialize` their
2414/// requests). Bincode encodes an enum's variant as a 4-byte little-endian
2415/// `u32` discriminant, so the *order* of the variants is the wire contract:
2416/// reordering or inserting a variant anywhere but the end silently reassigns
2417/// every following tag and breaks already-deployed clients (the v0.2.11
2418/// break class).
2419///
2420/// `RegisterDelegateWithPredecessors` was appended as the LAST variant
2421/// precisely so the three pre-existing tags stay byte-identical. These tests
2422/// exist because, before this module, `DelegateRequest` had NO wire-format
2423/// pin at all — a reorder would have shipped undetected.
2424#[cfg(test)]
2425mod delegate_request_wire_format {
2426    use super::DelegateRequest;
2427    use crate::code_hash::CodeHash;
2428    use crate::prelude::{
2429        ApplicationMessage, Delegate, DelegateCode, DelegateContainer, DelegateKey,
2430        DelegateWasmAPIVersion, InboundDelegateMsg, Parameters,
2431    };
2432
2433    fn sample_container() -> DelegateContainer {
2434        let code = DelegateCode::from(vec![1u8, 2, 3, 4]);
2435        let params = Parameters::from(vec![9u8, 8, 7]);
2436        DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((&code, &params))))
2437    }
2438
2439    fn sample_key(fill: u8) -> DelegateKey {
2440        DelegateKey::new([fill; 32], CodeHash::new([fill.wrapping_add(1); 32]))
2441    }
2442
2443    // The four sample values whose complete bincode encodings are frozen in
2444    // `wire_format_is_frozen`. Kept byte-for-byte identical to the throwaway
2445    // generator that produced the frozen vectors, so the freeze is
2446    // reproducible: construct the value, `bincode::serialize`, compare.
2447    fn sample_app_messages() -> DelegateRequest<'static> {
2448        DelegateRequest::ApplicationMessages {
2449            key: DelegateKey::new([0x11; 32], CodeHash::new([0x22; 32])),
2450            params: Parameters::from(vec![0xDE, 0xAD, 0xBE, 0xEF]),
2451            inbound: vec![InboundDelegateMsg::ApplicationMessage(
2452                ApplicationMessage::new(vec![0x01, 0x02, 0x03]),
2453            )],
2454        }
2455    }
2456
2457    fn sample_register() -> DelegateRequest<'static> {
2458        DelegateRequest::RegisterDelegate {
2459            delegate: sample_container(),
2460            cipher: [0x55; 32],
2461            nonce: [0x66; 24],
2462        }
2463    }
2464
2465    fn sample_unregister() -> DelegateRequest<'static> {
2466        DelegateRequest::UnregisterDelegate(DelegateKey::new([0x11; 32], CodeHash::new([0x22; 32])))
2467    }
2468
2469    fn sample_register_with_predecessors() -> DelegateRequest<'static> {
2470        DelegateRequest::RegisterDelegateWithPredecessors {
2471            delegate: sample_container(),
2472            cipher: [0x33; 32],
2473            nonce: [0x44; 24],
2474            predecessors: vec![sample_key(0xA0), sample_key(0xB0), sample_key(0xC0)],
2475        }
2476    }
2477
2478    /// Complete-byte wire-format freeze for all four variants.
2479    ///
2480    /// The three pre-existing variants (`ApplicationMessages`,
2481    /// `RegisterDelegate`, `UnregisterDelegate`) are pinned to their FULL
2482    /// expected byte vectors — not just the 4-byte tag — so a field reorder or
2483    /// a change to a nested type's encoding (e.g. `DelegateContainer`,
2484    /// `DelegateKey`, `ApplicationMessage`) is caught, not only a variant
2485    /// reorder. Those three vectors were generated from **origin/main @
2486    /// 8b53702** (the shipped format) via a throwaway generator and confirmed
2487    /// byte-identical to this branch's output, so the pin anchors to what old
2488    /// clients actually speak. The fourth vector
2489    /// (`RegisterDelegateWithPredecessors`, multi-predecessor list) was
2490    /// generated from THIS branch.
2491    ///
2492    /// The test also DESERIALIZES each frozen vector, proving an old-format
2493    /// byte stream still decodes into the expected variant on this build.
2494    #[test]
2495    fn wire_format_is_frozen() {
2496        // --- origin/main @ 8b53702 (shipped format) ---
2497        const APP_MESSAGES: &[u8] = &[
2498            0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
2499            17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2500            34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2501            34, 4, 0, 0, 0, 0, 0, 0, 0, 222, 173, 190, 239, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3,
2502            0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2503        ];
2504        const REGISTER: &[u8] = &[
2505            1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 4, 0, 0, 0, 0, 0,
2506            0, 0, 1, 2, 3, 4, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5, 141, 135, 18, 213, 208,
2507            81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160, 84, 50, 88, 111, 44, 39,
2508            24, 219, 97, 92, 222, 20, 205, 248, 149, 154, 214, 38, 193, 144, 31, 141, 32, 222, 49,
2509            197, 66, 237, 16, 98, 165, 72, 6, 11, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5,
2510            141, 135, 18, 213, 208, 81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160,
2511            84, 50, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
2512            85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 102, 102, 102, 102, 102, 102, 102, 102,
2513            102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
2514        ];
2515        const UNREGISTER: &[u8] = &[
2516            2, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
2517            17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2518            34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2519            34,
2520        ];
2521        // --- this branch (the new variant) ---
2522        const REGISTER_WITH_PREDECESSORS: &[u8] = &[
2523            3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 9, 8, 7, 4, 0, 0, 0, 0, 0,
2524            0, 0, 1, 2, 3, 4, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5, 141, 135, 18, 213, 208,
2525            81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160, 84, 50, 88, 111, 44, 39,
2526            24, 219, 97, 92, 222, 20, 205, 248, 149, 154, 214, 38, 193, 144, 31, 141, 32, 222, 49,
2527            197, 66, 237, 16, 98, 165, 72, 6, 11, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5,
2528            141, 135, 18, 213, 208, 81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160,
2529            84, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,
2530            51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
2531            68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 3, 0, 0, 0, 0, 0, 0, 0, 160,
2532            160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160,
2533            160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 161, 161, 161,
2534            161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161,
2535            161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 176, 176, 176, 176, 176,
2536            176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176,
2537            176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 177, 177, 177, 177, 177, 177, 177,
2538            177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177,
2539            177, 177, 177, 177, 177, 177, 177, 177, 192, 192, 192, 192, 192, 192, 192, 192, 192,
2540            192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
2541            192, 192, 192, 192, 192, 192, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193,
2542            193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193,
2543            193, 193, 193, 193,
2544        ];
2545
2546        // 1. Serialization is byte-stable for every variant.
2547        assert_eq!(
2548            bincode::serialize(&sample_app_messages()).unwrap(),
2549            APP_MESSAGES,
2550            "ApplicationMessages (tag 0) encoding changed"
2551        );
2552        assert_eq!(
2553            bincode::serialize(&sample_register()).unwrap(),
2554            REGISTER,
2555            "RegisterDelegate (tag 1) encoding changed"
2556        );
2557        assert_eq!(
2558            bincode::serialize(&sample_unregister()).unwrap(),
2559            UNREGISTER,
2560            "UnregisterDelegate (tag 2) encoding changed"
2561        );
2562        assert_eq!(
2563            bincode::serialize(&sample_register_with_predecessors()).unwrap(),
2564            REGISTER_WITH_PREDECESSORS,
2565            "RegisterDelegateWithPredecessors (tag 3) encoding changed"
2566        );
2567
2568        // 2. The four variant tags are exactly 0,1,2,3 — appending the new
2569        //    variant did not shift the pre-existing three.
2570        assert_eq!(APP_MESSAGES[..4], 0u32.to_le_bytes());
2571        assert_eq!(REGISTER[..4], 1u32.to_le_bytes());
2572        assert_eq!(UNREGISTER[..4], 2u32.to_le_bytes());
2573        assert_eq!(REGISTER_WITH_PREDECESSORS[..4], 3u32.to_le_bytes());
2574
2575        // 3. Each frozen (old-format) byte stream still DECODES into its
2576        //    variant on this build — an old client's bytes remain readable.
2577        assert!(matches!(
2578            bincode::deserialize::<DelegateRequest>(APP_MESSAGES).unwrap(),
2579            DelegateRequest::ApplicationMessages { .. }
2580        ));
2581        assert!(matches!(
2582            bincode::deserialize::<DelegateRequest>(REGISTER).unwrap(),
2583            DelegateRequest::RegisterDelegate { .. }
2584        ));
2585        assert!(matches!(
2586            bincode::deserialize::<DelegateRequest>(UNREGISTER).unwrap(),
2587            DelegateRequest::UnregisterDelegate(_)
2588        ));
2589        assert!(matches!(
2590            bincode::deserialize::<DelegateRequest>(REGISTER_WITH_PREDECESSORS).unwrap(),
2591            DelegateRequest::RegisterDelegateWithPredecessors { .. }
2592        ));
2593    }
2594
2595    /// The new variant's tag is exactly `3u32` little-endian — a direct,
2596    /// self-documenting assertion that it was appended one past
2597    /// `UnregisterDelegate` (tag 2).
2598    #[test]
2599    fn register_with_predecessors_tag_is_three() {
2600        let req = DelegateRequest::RegisterDelegateWithPredecessors {
2601            delegate: sample_container(),
2602            cipher: [0; 32],
2603            nonce: [0; 24],
2604            predecessors: vec![],
2605        };
2606        assert_eq!(
2607            bincode::serialize(&req).unwrap()[..4],
2608            3u32.to_le_bytes(),
2609            "RegisterDelegateWithPredecessors must be the 4th variant (tag 3)"
2610        );
2611    }
2612
2613    /// The new variant round-trips through bincode, including a multi-element
2614    /// `predecessors` list (the skip-generations case the variant exists for).
2615    #[test]
2616    fn register_with_predecessors_round_trips() {
2617        let predecessors = vec![sample_key(0xA0), sample_key(0xB0), sample_key(0xC0)];
2618        let delegate = sample_container();
2619        let expected_key = delegate.key().clone();
2620
2621        let req = DelegateRequest::RegisterDelegateWithPredecessors {
2622            delegate,
2623            cipher: [0x33; 32],
2624            nonce: [0x44; 24],
2625            predecessors: predecessors.clone(),
2626        };
2627
2628        let bytes = bincode::serialize(&req).unwrap();
2629        let decoded: DelegateRequest = bincode::deserialize(&bytes).unwrap();
2630
2631        match decoded {
2632            DelegateRequest::RegisterDelegateWithPredecessors {
2633                delegate,
2634                cipher,
2635                nonce,
2636                predecessors: got,
2637            } => {
2638                assert_eq!(delegate.key(), &expected_key, "delegate preserved");
2639                assert_eq!(cipher, [0x33; 32], "cipher preserved");
2640                assert_eq!(nonce, [0x44; 24], "nonce preserved");
2641                assert_eq!(
2642                    got, predecessors,
2643                    "full predecessor list preserved in order"
2644                );
2645            }
2646            other => panic!("round-trip produced the wrong variant: {other:?}"),
2647        }
2648    }
2649
2650    /// `key()` returns the newly-registered delegate's key (not a
2651    /// predecessor's), matching `RegisterDelegate`'s behavior.
2652    #[test]
2653    fn key_returns_the_new_delegate() {
2654        let delegate = sample_container();
2655        let expected_key = delegate.key().clone();
2656        let req = DelegateRequest::RegisterDelegateWithPredecessors {
2657            delegate,
2658            cipher: [0; 32],
2659            nonce: [0; 24],
2660            predecessors: vec![sample_key(0xA0)],
2661        };
2662        assert_eq!(req.key(), &expected_key);
2663    }
2664
2665    /// The flatbuffers decode path must NOT panic on an unknown
2666    /// `DelegateRequestType` discriminant. The generated union verifier accepts
2667    /// any discriminant it doesn't recognize (`_ => Ok(())`), and the union
2668    /// type field is a raw `u8` that the public (TypeScript) builder can set to
2669    /// any value, so a crafted request reaches `DelegateRequest::try_decode_fbs`
2670    /// with an out-of-range discriminant. Before the fix this hit
2671    /// `unreachable!()` and took down the connection handler; now it is a clean
2672    /// per-request error. (Regression guard for the P2 finding on PR #86.)
2673    #[test]
2674    fn fbs_decode_rejects_unknown_discriminant() {
2675        use crate::client_api::TryFromFbs;
2676        use crate::generated::client_request::{
2677            finish_client_request_buffer, root_as_client_request,
2678            ClientRequest as FbsClientRequest, ClientRequestArgs, ClientRequestType,
2679            DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateRequest as FbsDelegateRequest,
2680            DelegateRequestArgs, DelegateRequestType, UnregisterDelegate, UnregisterDelegateArgs,
2681        };
2682
2683        let mut b = flatbuffers::FlatBufferBuilder::new();
2684        // A real, well-formed UnregisterDelegate table to hang off the union...
2685        let key = b.create_vector(&[0u8; 32]);
2686        let code_hash = b.create_vector(&[0u8; 32]);
2687        let dk = FbsDelegateKey::create(
2688            &mut b,
2689            &DelegateKeyArgs {
2690                key: Some(key),
2691                code_hash: Some(code_hash),
2692            },
2693        );
2694        let unreg = UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
2695        // ...but LIE about the union type: 99 is past the max known
2696        // discriminant (UnregisterDelegate = 3), yet the verifier accepts it.
2697        let dreq = FbsDelegateRequest::create(
2698            &mut b,
2699            &DelegateRequestArgs {
2700                delegate_request_type: DelegateRequestType(99),
2701                delegate_request: Some(unreg.as_union_value()),
2702            },
2703        );
2704        let creq = FbsClientRequest::create(
2705            &mut b,
2706            &ClientRequestArgs {
2707                client_request_type: ClientRequestType::DelegateRequest,
2708                client_request: Some(dreq.as_union_value()),
2709            },
2710        );
2711        finish_client_request_buffer(&mut b, creq);
2712        let bytes = b.finished_data().to_vec();
2713
2714        let client =
2715            root_as_client_request(&bytes).expect("verifier accepts an unknown union discriminant");
2716        let fbs_delegate = client
2717            .client_request_as_delegate_request()
2718            .expect("client_request is a DelegateRequest");
2719        let decoded = DelegateRequest::try_decode_fbs(&fbs_delegate);
2720        assert!(
2721            decoded.is_err(),
2722            "an unknown DelegateRequestType discriminant must be a clean \
2723             per-request error, never a panic that downs the connection handler"
2724        );
2725    }
2726}
2727
2728/// Hardening pins for the flatbuffers decode boundary.
2729///
2730/// Every test here exists because a real decode site panicked, or silently
2731/// produced a wrong value, on input a client can actually send. The boundary
2732/// has exactly one entry point — [`ClientRequest::try_decode_fbs`] over
2733/// `root_as_client_request` — so these drive the real entry point rather than
2734/// an inner decoder wherever possible.
2735///
2736/// Two facts about the flatbuffers verifier make this whole class possible, and
2737/// both are load-bearing for every test below:
2738///
2739/// 1. A `(required)` vector is checked for PRESENCE, not LENGTH
2740///    (`Verifiable for Vector<T>` runs `verify_vector_range` and stops there).
2741/// 2. Every generated union verifier ends in `_ => Ok(())`, so an unknown
2742///    discriminant — including `NONE` — reaches the decoder's match.
2743#[cfg(test)]
2744mod fbs_decode_hardening {
2745    use super::{ClientRequest, ContractRequest};
2746    use crate::client_api::TryFromFbs;
2747    use crate::contract_interface::UpdateData;
2748    use crate::generated::client_request::{
2749        finish_client_request_buffer, ApplicationMessages, ApplicationMessagesArgs,
2750        ClientRequest as FbsClientRequest, ClientRequestArgs, ClientRequestType,
2751        ContractRequest as FbsContractRequest, ContractRequestArgs, ContractRequestType,
2752        DelegateCode as FbsDelegateCode, DelegateCodeArgs,
2753        DelegateContainer as FbsDelegateContainer, DelegateContainerArgs,
2754        DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateRequest as FbsDelegateRequest,
2755        DelegateRequestArgs, DelegateRequestType, DelegateType, Get as FbsGet, GetArgs,
2756        InboundDelegateMsg as FbsInboundDelegateMsg, InboundDelegateMsgArgs,
2757        InboundDelegateMsgType, Put as FbsPut, PutArgs, RegisterDelegate, RegisterDelegateArgs,
2758        RelatedContract, RelatedContractArgs, RelatedContracts as FbsRelatedContracts,
2759        RelatedContractsArgs, Update as FbsUpdate, UpdateArgs, WasmDelegateV1, WasmDelegateV1Args,
2760    };
2761    use crate::generated::common::{
2762        ApplicationMessage as FbsApplicationMessage, ApplicationMessageArgs,
2763        ContractCode as FbsContractCode, ContractCodeArgs,
2764        ContractContainer as FbsContractContainer, ContractContainerArgs,
2765        ContractInstanceId as FbsContractInstanceId, ContractInstanceIdArgs,
2766        ContractKey as FbsContractKey, ContractKeyArgs, ContractType, RelatedDeltaUpdate,
2767        RelatedDeltaUpdateArgs, RelatedStateAndDeltaUpdate, RelatedStateAndDeltaUpdateArgs,
2768        RelatedStateUpdate, RelatedStateUpdateArgs, StateUpdate, StateUpdateArgs,
2769        UpdateData as FbsUpdateData, UpdateDataArgs, UpdateDataType, WasmContractV1,
2770        WasmContractV1Args,
2771    };
2772
2773    type Builder<'a> = flatbuffers::FlatBufferBuilder<'a>;
2774
2775    /// The instance id the tests round-trip. Deliberately NOT all-ASCII: this is
2776    /// what a real 32-byte id looks like, and it is precisely what the old
2777    /// base58 decode choked on.
2778    const INSTANCE: [u8; 32] = [
2779        0x00, 0xff, 0x7a, 0x01, 0x30, 0x4f, 0x49, 0x6c, 0x2b, 0x2f, 0x5c, 0x7f, 0x80, 0xfe, 0x10,
2780        0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
2781        0x20, 0x21,
2782    ];
2783    const CODE_HASH: [u8; 32] = [42u8; 32];
2784
2785    fn instance_offset<'a>(
2786        b: &mut Builder<'a>,
2787        bytes: &[u8],
2788    ) -> flatbuffers::WIPOffset<FbsContractInstanceId<'a>> {
2789        let data = b.create_vector(bytes);
2790        FbsContractInstanceId::create(b, &ContractInstanceIdArgs { data: Some(data) })
2791    }
2792
2793    fn key_offset<'a>(
2794        b: &mut Builder<'a>,
2795        instance: &[u8],
2796        code: &[u8],
2797    ) -> flatbuffers::WIPOffset<FbsContractKey<'a>> {
2798        let instance = instance_offset(b, instance);
2799        let code = b.create_vector(code);
2800        FbsContractKey::create(
2801            b,
2802            &ContractKeyArgs {
2803                instance: Some(instance),
2804                code: Some(code),
2805            },
2806        )
2807    }
2808
2809    fn delegate_key_offset<'a>(
2810        b: &mut Builder<'a>,
2811        key: &[u8],
2812        code_hash: &[u8],
2813    ) -> flatbuffers::WIPOffset<FbsDelegateKey<'a>> {
2814        let key = b.create_vector(key);
2815        let code_hash = b.create_vector(code_hash);
2816        FbsDelegateKey::create(
2817            b,
2818            &DelegateKeyArgs {
2819                key: Some(key),
2820                code_hash: Some(code_hash),
2821            },
2822        )
2823    }
2824
2825    /// Finish a `ContractRequest`-carrying `ClientRequest` and return its bytes.
2826    fn finish_contract(
2827        b: &mut Builder<'_>,
2828        ty: ContractRequestType,
2829        req: flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
2830    ) -> Vec<u8> {
2831        let contract = FbsContractRequest::create(
2832            b,
2833            &ContractRequestArgs {
2834                contract_request_type: ty,
2835                contract_request: Some(req),
2836            },
2837        );
2838        let client = FbsClientRequest::create(
2839            b,
2840            &ClientRequestArgs {
2841                client_request_type: ClientRequestType::ContractRequest,
2842                client_request: Some(contract.as_union_value()),
2843            },
2844        );
2845        finish_client_request_buffer(b, client);
2846        b.finished_data().to_vec()
2847    }
2848
2849    fn finish_delegate(
2850        b: &mut Builder<'_>,
2851        ty: DelegateRequestType,
2852        req: flatbuffers::WIPOffset<flatbuffers::UnionWIPOffset>,
2853    ) -> Vec<u8> {
2854        let delegate = FbsDelegateRequest::create(
2855            b,
2856            &DelegateRequestArgs {
2857                delegate_request_type: ty,
2858                delegate_request: Some(req),
2859            },
2860        );
2861        let client = FbsClientRequest::create(
2862            b,
2863            &ClientRequestArgs {
2864                client_request_type: ClientRequestType::DelegateRequest,
2865                client_request: Some(delegate.as_union_value()),
2866            },
2867        );
2868        finish_client_request_buffer(b, client);
2869        b.finished_data().to_vec()
2870    }
2871
2872    // ---------------------------------------------------------------------
2873    // Per-union builders. Each produces a well-formed request whose ONE named
2874    // union discriminant is `d`, so a test can sweep `d` while everything else
2875    // stays valid.
2876    // ---------------------------------------------------------------------
2877
2878    fn client_request_type(d: u8, force_defaults: bool) -> Vec<u8> {
2879        let mut b = Builder::new();
2880        b.force_defaults(force_defaults);
2881        let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
2882        let get = FbsGet::create(
2883            &mut b,
2884            &GetArgs {
2885                key: Some(key),
2886                fetch_contract: false,
2887                subscribe: false,
2888                blocking_subscribe: false,
2889            },
2890        );
2891        let contract = FbsContractRequest::create(
2892            &mut b,
2893            &ContractRequestArgs {
2894                contract_request_type: ContractRequestType::Get,
2895                contract_request: Some(get.as_union_value()),
2896            },
2897        );
2898        let client = FbsClientRequest::create(
2899            &mut b,
2900            &ClientRequestArgs {
2901                client_request_type: ClientRequestType(d),
2902                client_request: Some(contract.as_union_value()),
2903            },
2904        );
2905        finish_client_request_buffer(&mut b, client);
2906        b.finished_data().to_vec()
2907    }
2908
2909    fn contract_request_type(d: u8, force_defaults: bool) -> Vec<u8> {
2910        let mut b = Builder::new();
2911        b.force_defaults(force_defaults);
2912        let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
2913        let get = FbsGet::create(
2914            &mut b,
2915            &GetArgs {
2916                key: Some(key),
2917                fetch_contract: false,
2918                subscribe: false,
2919                blocking_subscribe: false,
2920            },
2921        );
2922        finish_contract(&mut b, ContractRequestType(d), get.as_union_value())
2923    }
2924
2925    fn delegate_request_type(d: u8, force_defaults: bool) -> Vec<u8> {
2926        let mut b = Builder::new();
2927        b.force_defaults(force_defaults);
2928        let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
2929        let params = b.create_vector(&[1u8, 2, 3]);
2930        let payload = b.create_vector(&[9u8; 4]);
2931        let context = b.create_vector(&[0u8; 2]);
2932        let app = FbsApplicationMessage::create(
2933            &mut b,
2934            &ApplicationMessageArgs {
2935                payload: Some(payload),
2936                context: Some(context),
2937                processed: false,
2938            },
2939        );
2940        let inbound_msg = FbsInboundDelegateMsg::create(
2941            &mut b,
2942            &InboundDelegateMsgArgs {
2943                inbound_type: InboundDelegateMsgType::common_ApplicationMessage,
2944                inbound: Some(app.as_union_value()),
2945            },
2946        );
2947        let inbound = b.create_vector(&[inbound_msg]);
2948        let msgs = ApplicationMessages::create(
2949            &mut b,
2950            &ApplicationMessagesArgs {
2951                key: Some(dk),
2952                params: Some(params),
2953                inbound: Some(inbound),
2954            },
2955        );
2956        finish_delegate(&mut b, DelegateRequestType(d), msgs.as_union_value())
2957    }
2958
2959    /// A PUT whose `common.ContractContainer` union discriminant is `d`.
2960    fn contract_type(d: u8, force_defaults: bool) -> Vec<u8> {
2961        let mut b = Builder::new();
2962        b.force_defaults(force_defaults);
2963        let code_data = b.create_vector(&[0u8; 8]);
2964        let code_hash = b.create_vector(&CODE_HASH);
2965        let code = FbsContractCode::create(
2966            &mut b,
2967            &ContractCodeArgs {
2968                data: Some(code_data),
2969                code_hash: Some(code_hash),
2970            },
2971        );
2972        let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
2973        let params = b.create_vector(&[1u8, 2]);
2974        let wasm = WasmContractV1::create(
2975            &mut b,
2976            &WasmContractV1Args {
2977                data: Some(code),
2978                parameters: Some(params),
2979                key: Some(key),
2980            },
2981        );
2982        let container = FbsContractContainer::create(
2983            &mut b,
2984            &ContractContainerArgs {
2985                contract_type: ContractType(d),
2986                contract: Some(wasm.as_union_value()),
2987            },
2988        );
2989        let state = b.create_vector(&[3u8; 4]);
2990        let empty: Vec<flatbuffers::WIPOffset<RelatedContract>> = vec![];
2991        let contracts = b.create_vector(&empty);
2992        let related = FbsRelatedContracts::create(
2993            &mut b,
2994            &RelatedContractsArgs {
2995                contracts: Some(contracts),
2996            },
2997        );
2998        let put = FbsPut::create(
2999            &mut b,
3000            &PutArgs {
3001                container: Some(container),
3002                wrapped_state: Some(state),
3003                related_contracts: Some(related),
3004                subscribe: false,
3005                blocking_subscribe: false,
3006            },
3007        );
3008        finish_contract(&mut b, ContractRequestType::Put, put.as_union_value())
3009    }
3010
3011    /// A RegisterDelegate whose `DelegateContainer` union discriminant is `d`.
3012    fn delegate_type(d: u8, force_defaults: bool) -> Vec<u8> {
3013        let mut b = Builder::new();
3014        b.force_defaults(force_defaults);
3015        let code_data = b.create_vector(&[0u8; 8]);
3016        let code_hash = b.create_vector(&CODE_HASH);
3017        let code = FbsDelegateCode::create(
3018            &mut b,
3019            &DelegateCodeArgs {
3020                data: Some(code_data),
3021                code_hash: Some(code_hash),
3022            },
3023        );
3024        let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
3025        let params = b.create_vector(&[1u8, 2]);
3026        let wasm = WasmDelegateV1::create(
3027            &mut b,
3028            &WasmDelegateV1Args {
3029                parameters: Some(params),
3030                data: Some(code),
3031                key: Some(dk),
3032            },
3033        );
3034        let container = FbsDelegateContainer::create(
3035            &mut b,
3036            &DelegateContainerArgs {
3037                delegate_type: DelegateType(d),
3038                delegate: Some(wasm.as_union_value()),
3039            },
3040        );
3041        let cipher = b.create_vector(&[1u8; 32]);
3042        let nonce = b.create_vector(&[2u8; 24]);
3043        let register = RegisterDelegate::create(
3044            &mut b,
3045            &RegisterDelegateArgs {
3046                delegate: Some(container),
3047                cipher: Some(cipher),
3048                nonce: Some(nonce),
3049            },
3050        );
3051        finish_delegate(
3052            &mut b,
3053            DelegateRequestType::RegisterDelegate,
3054            register.as_union_value(),
3055        )
3056    }
3057
3058    /// An UPDATE whose `common.UpdateData` union discriminant is `d`.
3059    fn update_data_type(d: u8, force_defaults: bool) -> Vec<u8> {
3060        let mut b = Builder::new();
3061        b.force_defaults(force_defaults);
3062        let state = b.create_vector(&[5u8; 4]);
3063        let state_update = StateUpdate::create(&mut b, &StateUpdateArgs { state: Some(state) });
3064        let data = FbsUpdateData::create(
3065            &mut b,
3066            &UpdateDataArgs {
3067                update_data_type: UpdateDataType(d),
3068                update_data: Some(state_update.as_union_value()),
3069            },
3070        );
3071        let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
3072        let update = FbsUpdate::create(
3073            &mut b,
3074            &UpdateArgs {
3075                key: Some(key),
3076                data: Some(data),
3077            },
3078        );
3079        finish_contract(&mut b, ContractRequestType::Update, update.as_union_value())
3080    }
3081
3082    /// An ApplicationMessages whose `InboundDelegateMsg` union discriminant is `d`.
3083    fn inbound_delegate_msg_type(d: u8, force_defaults: bool) -> Vec<u8> {
3084        let mut b = Builder::new();
3085        b.force_defaults(force_defaults);
3086        let payload = b.create_vector(&[9u8; 4]);
3087        let context = b.create_vector(&[0u8; 2]);
3088        let app = FbsApplicationMessage::create(
3089            &mut b,
3090            &ApplicationMessageArgs {
3091                payload: Some(payload),
3092                context: Some(context),
3093                processed: false,
3094            },
3095        );
3096        let inbound_msg = FbsInboundDelegateMsg::create(
3097            &mut b,
3098            &InboundDelegateMsgArgs {
3099                inbound_type: InboundDelegateMsgType(d),
3100                inbound: Some(app.as_union_value()),
3101            },
3102        );
3103        let inbound = b.create_vector(&[inbound_msg]);
3104        let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
3105        let params = b.create_vector(&[1u8, 2, 3]);
3106        let msgs = ApplicationMessages::create(
3107            &mut b,
3108            &ApplicationMessagesArgs {
3109                key: Some(dk),
3110                params: Some(params),
3111                inbound: Some(inbound),
3112            },
3113        );
3114        finish_delegate(
3115            &mut b,
3116            DelegateRequestType::ApplicationMessages,
3117            msgs.as_union_value(),
3118        )
3119    }
3120
3121    /// `(union name, builder, the union's real discriminant)`.
3122    type UnionCase = (&'static str, fn(u8, bool) -> Vec<u8>, u8);
3123
3124    /// **No union discriminant, for any of the seven unions on the decode path,
3125    /// may panic the decoder.**
3126    ///
3127    /// One sweep covering every union at once. Four of the seven were
3128    /// `unreachable!()` before this change, and all four are reachable: the
3129    /// generated verifiers end in `_ => Ok(())`, so an unrecognized discriminant
3130    /// passes verification and lands in the decoder's match.
3131    ///
3132    /// Scope, stated honestly: this covers a new *variant* on an existing union
3133    /// for free, but NOT a new *union* — `cases` and its builders are written by
3134    /// hand. `union_matches_never_use_unreachable` is the guard for that case.
3135    ///
3136    /// Four properties are pinned per union, and the third is the one that keeps
3137    /// the other three honest:
3138    ///
3139    /// - The 0..=255 sweep RETURNS. A panic anywhere inside fails the test.
3140    /// - An out-of-range discriminant is a clean error.
3141    /// - That error came from the DECODER, not the verifier. Without this the
3142    ///   sweep silently degrades to zero decoder coverage the moment a schema or
3143    ///   builder change makes verification fail earlier — which is exactly what
3144    ///   had happened to the NONE case below.
3145    /// - The real discriminant still decodes, so the guard did not break the
3146    ///   happy path.
3147    #[test]
3148    fn no_union_discriminant_panics_the_decoder() {
3149        let cases: [UnionCase; 7] = [
3150            ("ClientRequestType", client_request_type, 1),
3151            ("ContractRequestType", contract_request_type, 3),
3152            ("DelegateRequestType", delegate_request_type, 1),
3153            ("ContractType", contract_type, 1),
3154            ("DelegateType", delegate_type, 1),
3155            ("UpdateDataType", update_data_type, 1),
3156            (
3157                "InboundDelegateMsgType",
3158                inbound_delegate_msg_type,
3159                InboundDelegateMsgType::common_ApplicationMessage.0,
3160            ),
3161        ];
3162
3163        for (union, build, valid) in cases {
3164            for d in 0..=u8::MAX {
3165                // Must return, never panic. A mismatched-but-known discriminant
3166                // may be rejected by the verifier rather than the decoder;
3167                // either is a clean error and both are acceptable here.
3168                let bytes = build(d, false);
3169                let _ = ClientRequest::try_decode_fbs(&bytes);
3170            }
3171
3172            // An out-of-range discriminant must be rejected BY THE DECODER.
3173            // Asserting only `is_err()` would also pass on a verifier
3174            // rejection, which reaches none of the code this change touches.
3175            let out_of_range = build(200, false);
3176            let err = ClientRequest::try_decode_fbs(&out_of_range)
3177                .expect_err("{union}: an out-of-range discriminant must be a clean error");
3178            assert_eq!(
3179                err.to_string(),
3180                format!(
3181                    "Failed decoding message from client request: unknown {union} \
3182                     discriminant: 200"
3183                ),
3184                "{union}: the error must come from the decoder's union arm, not \
3185                 from the verifier — otherwise this sweep pins nothing"
3186            );
3187
3188            // `NONE` needs `force_defaults`: both builders elide a field equal
3189            // to its default, so an ordinary `NONE` is written as ABSENT and
3190            // `visit_union` rejects the present-value/absent-type pair before
3191            // any decoder runs. Forcing defaults writes the zero explicitly,
3192            // which is the shape a non-flatc encoder can put on the wire and
3193            // the only one that actually reaches the match arm.
3194            let none = build(0, true);
3195            let err = ClientRequest::try_decode_fbs(&none)
3196                .expect_err("{union}: a NONE discriminant must be a clean error");
3197            assert_eq!(
3198                err.to_string(),
3199                format!(
3200                    "Failed decoding message from client request: unknown {union} \
3201                     discriminant: 0"
3202                ),
3203                "{union}: an explicit NONE must reach the decoder's union arm"
3204            );
3205
3206            let good = build(valid, false);
3207            assert!(
3208                ClientRequest::try_decode_fbs(&good).is_ok(),
3209                "{union}: the real discriminant must still decode; the guard \
3210                 must not break the happy path"
3211            );
3212        }
3213    }
3214
3215    // ---------------------------------------------------------------------
3216    // Per-site pins. Each of these fails if its own decode site is reverted,
3217    // so a partial revert cannot leave the suite green — the failure mode #85
3218    // shipped with, where the fix reached three call sites and only one was
3219    // pinned.
3220    // ---------------------------------------------------------------------
3221
3222    /// Build an UPDATE carrying one of the three `related_*` update variants,
3223    /// with `id` as the raw `related_to` bytes.
3224    fn update_with_related(variant: UpdateDataType, id: &[u8]) -> Vec<u8> {
3225        let mut b = Builder::new();
3226        let related_to = instance_offset(&mut b, id);
3227        let payload = b.create_vector(&[5u8; 4]);
3228        let data_offset = match variant {
3229            UpdateDataType::RelatedStateUpdate => RelatedStateUpdate::create(
3230                &mut b,
3231                &RelatedStateUpdateArgs {
3232                    related_to: Some(related_to),
3233                    state: Some(payload),
3234                },
3235            )
3236            .as_union_value(),
3237            UpdateDataType::RelatedDeltaUpdate => RelatedDeltaUpdate::create(
3238                &mut b,
3239                &RelatedDeltaUpdateArgs {
3240                    related_to: Some(related_to),
3241                    delta: Some(payload),
3242                },
3243            )
3244            .as_union_value(),
3245            UpdateDataType::RelatedStateAndDeltaUpdate => {
3246                let delta = b.create_vector(&[6u8; 4]);
3247                RelatedStateAndDeltaUpdate::create(
3248                    &mut b,
3249                    &RelatedStateAndDeltaUpdateArgs {
3250                        related_to: Some(related_to),
3251                        state: Some(payload),
3252                        delta: Some(delta),
3253                    },
3254                )
3255                .as_union_value()
3256            }
3257            other => panic!("not a related update variant: {}", other.0),
3258        };
3259        let data = FbsUpdateData::create(
3260            &mut b,
3261            &UpdateDataArgs {
3262                update_data_type: variant,
3263                update_data: Some(data_offset),
3264            },
3265        );
3266        let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
3267        let update = FbsUpdate::create(
3268            &mut b,
3269            &UpdateArgs {
3270                key: Some(key),
3271                data: Some(data),
3272            },
3273        );
3274        finish_contract(&mut b, ContractRequestType::Update, update.as_union_value())
3275    }
3276
3277    fn decoded_related_to(bytes: &[u8]) -> [u8; 32] {
3278        let req = ClientRequest::try_decode_fbs(bytes)
3279            .expect("a well-formed related update must decode, not panic");
3280        let ClientRequest::ContractOp(ContractRequest::Update { data, .. }) = req else {
3281            panic!("expected an UPDATE, got {req:?}");
3282        };
3283        match data {
3284            UpdateData::RelatedState { related_to, .. }
3285            | UpdateData::RelatedDelta { related_to, .. }
3286            | UpdateData::RelatedStateAndDelta { related_to, .. } => *related_to,
3287            other => panic!("expected a related update, got {other:?}"),
3288        }
3289    }
3290
3291    /// `related_to` is 32 RAW bytes and must round-trip byte-for-byte.
3292    ///
3293    /// This decode used to be `ContractInstanceId::from_bytes(..).unwrap()` —
3294    /// a base58 *string* decoder pointed at bytes that are already the final
3295    /// id. It did not merely mishandle malformed input: a random 32-byte id
3296    /// essentially never consists solely of base58 characters, so EVERY
3297    /// well-formed related update panicked the connection task. `INSTANCE`
3298    /// contains `0x00`, `0xff` and the base58-excluded `0`/`O`/`I`/`l`
3299    /// characters precisely so a revert cannot pass by luck.
3300    #[test]
3301    fn related_state_update_round_trips_the_raw_instance_id() {
3302        let bytes = update_with_related(UpdateDataType::RelatedStateUpdate, &INSTANCE);
3303        assert_eq!(decoded_related_to(&bytes), INSTANCE);
3304    }
3305
3306    #[test]
3307    fn related_delta_update_round_trips_the_raw_instance_id() {
3308        let bytes = update_with_related(UpdateDataType::RelatedDeltaUpdate, &INSTANCE);
3309        assert_eq!(decoded_related_to(&bytes), INSTANCE);
3310    }
3311
3312    #[test]
3313    fn related_state_and_delta_update_round_trips_the_raw_instance_id() {
3314        let bytes = update_with_related(UpdateDataType::RelatedStateAndDeltaUpdate, &INSTANCE);
3315        assert_eq!(decoded_related_to(&bytes), INSTANCE);
3316    }
3317
3318    /// A wrong-length `related_to` is a clean error naming the field, not a
3319    /// panic and not a silently truncated id.
3320    ///
3321    /// Parameterized over all three variants: with one case, copy-pasting the
3322    /// wrong variant's field name into the other two arms would fail nothing.
3323    #[test]
3324    fn related_to_wrong_length_is_rejected() {
3325        for (variant, field) in [
3326            (
3327                UpdateDataType::RelatedStateUpdate,
3328                "RelatedStateUpdate.related_to.data",
3329            ),
3330            (
3331                UpdateDataType::RelatedDeltaUpdate,
3332                "RelatedDeltaUpdate.related_to.data",
3333            ),
3334            (
3335                UpdateDataType::RelatedStateAndDeltaUpdate,
3336                "RelatedStateAndDeltaUpdate.related_to.data",
3337            ),
3338        ] {
3339            let bytes = update_with_related(variant, &[1u8; 8]);
3340            let err = ClientRequest::try_decode_fbs(&bytes)
3341                .expect_err("an 8-byte related_to must be rejected");
3342            let msg = err.to_string();
3343            assert!(
3344                msg.contains(field) && msg.contains("got 8 bytes"),
3345                "the error must name {field} and the observed length, got: {msg}"
3346            );
3347        }
3348    }
3349
3350    fn put_with_related_contract(id: &[u8]) -> Vec<u8> {
3351        let mut b = Builder::new();
3352        let code_data = b.create_vector(&[0u8; 8]);
3353        let code_hash = b.create_vector(&CODE_HASH);
3354        let code = FbsContractCode::create(
3355            &mut b,
3356            &ContractCodeArgs {
3357                data: Some(code_data),
3358                code_hash: Some(code_hash),
3359            },
3360        );
3361        let key = key_offset(&mut b, &INSTANCE, &CODE_HASH);
3362        let params = b.create_vector(&[1u8, 2]);
3363        let wasm = WasmContractV1::create(
3364            &mut b,
3365            &WasmContractV1Args {
3366                data: Some(code),
3367                parameters: Some(params),
3368                key: Some(key),
3369            },
3370        );
3371        let container = FbsContractContainer::create(
3372            &mut b,
3373            &ContractContainerArgs {
3374                contract_type: ContractType::WasmContractV1,
3375                contract: Some(wasm.as_union_value()),
3376            },
3377        );
3378        let related_id = instance_offset(&mut b, id);
3379        let related_state = b.create_vector(&[8u8; 3]);
3380        let related_contract = RelatedContract::create(
3381            &mut b,
3382            &RelatedContractArgs {
3383                instance_id: Some(related_id),
3384                state: Some(related_state),
3385            },
3386        );
3387        let contracts = b.create_vector(&[related_contract]);
3388        let related = FbsRelatedContracts::create(
3389            &mut b,
3390            &RelatedContractsArgs {
3391                contracts: Some(contracts),
3392            },
3393        );
3394        let state = b.create_vector(&[3u8; 4]);
3395        let put = FbsPut::create(
3396            &mut b,
3397            &PutArgs {
3398                container: Some(container),
3399                wrapped_state: Some(state),
3400                related_contracts: Some(related),
3401                subscribe: false,
3402                blocking_subscribe: false,
3403            },
3404        );
3405        finish_contract(&mut b, ContractRequestType::Put, put.as_union_value())
3406    }
3407
3408    /// The same base58 bug lived on the PUT path, in `RelatedContracts`. It went
3409    /// unnoticed because the loop body only runs when the vector is NON-empty,
3410    /// and the TypeScript suite's PUT fixture passes `new RelatedContractsT([])`.
3411    #[test]
3412    fn put_related_contract_round_trips_the_raw_instance_id() {
3413        let bytes = put_with_related_contract(&INSTANCE);
3414        let req = ClientRequest::try_decode_fbs(&bytes)
3415            .expect("a PUT carrying a related contract must decode, not panic");
3416        let ClientRequest::ContractOp(ContractRequest::Put {
3417            related_contracts, ..
3418        }) = req
3419        else {
3420            panic!("expected a PUT, got {req:?}");
3421        };
3422        let ids: Vec<[u8; 32]> = related_contracts
3423            .into_owned()
3424            .states()
3425            .map(|(id, _)| **id)
3426            .collect();
3427        assert_eq!(
3428            ids,
3429            vec![INSTANCE],
3430            "the related contract id must round-trip"
3431        );
3432    }
3433
3434    #[test]
3435    fn put_related_contract_wrong_length_id_is_rejected() {
3436        let bytes = put_with_related_contract(&[1u8; 8]);
3437        let err = ClientRequest::try_decode_fbs(&bytes)
3438            .expect_err("an 8-byte related contract id must be rejected");
3439        let msg = err.to_string();
3440        assert!(
3441            msg.contains("RelatedContract.instance_id") && msg.contains("got 8 bytes"),
3442            "got: {msg}"
3443        );
3444    }
3445
3446    fn unregister_delegate(key_len: usize) -> Vec<u8> {
3447        use crate::generated::client_request::{UnregisterDelegate, UnregisterDelegateArgs};
3448        let mut b = Builder::new();
3449        let dk = delegate_key_offset(&mut b, &vec![7u8; key_len], &CODE_HASH);
3450        let unregister =
3451            UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
3452        finish_delegate(
3453            &mut b,
3454            DelegateRequestType::UnregisterDelegate,
3455            unregister.as_union_value(),
3456        )
3457    }
3458
3459    /// `DelegateKey.key` is `(required)` but length-unchecked by the verifier,
3460    /// and the decoder used to `copy_from_slice` it into a `[0; 32]` — which
3461    /// panics on a mismatch. This is the normal delegate path, and the
3462    /// TypeScript SDK exports `DelegateKey` as the raw generated type with no
3463    /// length validation (unlike `ContractKey`, which throws a `TypeError`).
3464    #[test]
3465    fn delegate_key_wrong_length_is_rejected_not_panicking() {
3466        let short = unregister_delegate(8);
3467        let err = ClientRequest::try_decode_fbs(&short)
3468            .expect_err("an 8-byte delegate key must be rejected");
3469        let msg = err.to_string();
3470        assert!(
3471            msg.contains("DelegateKey.key") && msg.contains("got 8 bytes"),
3472            "got: {msg}"
3473        );
3474
3475        let long = unregister_delegate(64);
3476        let err = ClientRequest::try_decode_fbs(&long)
3477            .expect_err("a 64-byte delegate key must be rejected");
3478        assert!(err.to_string().contains("got 64 bytes"), "got: {err}");
3479
3480        let good = unregister_delegate(32);
3481        assert!(
3482            ClientRequest::try_decode_fbs(&good).is_ok(),
3483            "a 32-byte delegate key must still decode"
3484        );
3485    }
3486
3487    fn register_delegate(cipher_len: usize, nonce_len: usize) -> Vec<u8> {
3488        let mut b = Builder::new();
3489        let code_data = b.create_vector(&[0u8; 8]);
3490        let code_hash = b.create_vector(&CODE_HASH);
3491        let code = FbsDelegateCode::create(
3492            &mut b,
3493            &DelegateCodeArgs {
3494                data: Some(code_data),
3495                code_hash: Some(code_hash),
3496            },
3497        );
3498        let dk = delegate_key_offset(&mut b, &[7u8; 32], &CODE_HASH);
3499        let params = b.create_vector(&[1u8, 2]);
3500        let wasm = WasmDelegateV1::create(
3501            &mut b,
3502            &WasmDelegateV1Args {
3503                parameters: Some(params),
3504                data: Some(code),
3505                key: Some(dk),
3506            },
3507        );
3508        let container = FbsDelegateContainer::create(
3509            &mut b,
3510            &DelegateContainerArgs {
3511                delegate_type: DelegateType::WasmDelegateV1,
3512                delegate: Some(wasm.as_union_value()),
3513            },
3514        );
3515        let cipher = b.create_vector(&vec![1u8; cipher_len]);
3516        let nonce = b.create_vector(&vec![2u8; nonce_len]);
3517        let register = RegisterDelegate::create(
3518            &mut b,
3519            &RegisterDelegateArgs {
3520                delegate: Some(container),
3521                cipher: Some(cipher),
3522                nonce: Some(nonce),
3523            },
3524        );
3525        finish_delegate(
3526            &mut b,
3527            DelegateRequestType::RegisterDelegate,
3528            register.as_union_value(),
3529        )
3530    }
3531
3532    /// `cipher` (32) and `nonce` (24) are the same shape: `(required)`, so the
3533    /// verifier guarantees presence and nothing about length, and the decoder
3534    /// used to `try_from(..).unwrap()` them.
3535    #[test]
3536    fn register_delegate_wrong_length_cipher_or_nonce_is_rejected() {
3537        let err = ClientRequest::try_decode_fbs(&register_delegate(16, 24))
3538            .expect_err("a 16-byte cipher must be rejected");
3539        let msg = err.to_string();
3540        assert!(
3541            msg.contains("RegisterDelegate.cipher") && msg.contains("got 16 bytes"),
3542            "got: {msg}"
3543        );
3544
3545        let err = ClientRequest::try_decode_fbs(&register_delegate(32, 8))
3546            .expect_err("an 8-byte nonce must be rejected");
3547        let msg = err.to_string();
3548        assert!(
3549            msg.contains("RegisterDelegate.nonce") && msg.contains("got 8 bytes"),
3550            "got: {msg}"
3551        );
3552
3553        assert!(
3554            ClientRequest::try_decode_fbs(&register_delegate(32, 24)).is_ok(),
3555            "correct cipher/nonce lengths must still decode"
3556        );
3557    }
3558
3559    /// The ENCODE half of the same bug: `HostResponse`'s three related-update
3560    /// variants wrote `related_to.encode()` — base58 TEXT — into
3561    /// `common.ContractInstanceId.data`, a field every other producer and every
3562    /// consumer treats as 32 raw bytes.
3563    ///
3564    /// It survived because Rust only encodes host responses and only TypeScript
3565    /// decodes them, so no Rust round-trip test ever crossed it, and the
3566    /// TypeScript SDK has no test that reads `relatedTo` back out of a
3567    /// notification. Pinned here by decoding the encoder's own output, which is
3568    /// the cheapest thing that would have caught it.
3569    #[test]
3570    fn host_response_encodes_related_to_as_raw_bytes() {
3571        use crate::client_api::{ContractResponse, HostResponse};
3572        use crate::contract_interface::{ContractInstanceId, ContractKey, State};
3573        use crate::generated::host_response::{root_as_host_response, ContractResponseType};
3574
3575        let related = ContractInstanceId::new(INSTANCE);
3576        let key = ContractKey::from_params_and_code(
3577            crate::parameters::Parameters::from(vec![1u8, 2]),
3578            crate::contract_interface::ContractCode::from(vec![0u8; 8]),
3579        );
3580        let response = HostResponse::ContractResponse(ContractResponse::UpdateNotification {
3581            key,
3582            update: UpdateData::RelatedState {
3583                related_to: related,
3584                state: State::from(vec![9u8; 4]),
3585            },
3586        });
3587
3588        let bytes = response.into_fbs_bytes().expect("encoding must succeed");
3589        let host = root_as_host_response(&bytes).expect("the encoder must emit a valid buffer");
3590        let contract = host
3591            .response_as_contract_response()
3592            .expect("a ContractResponse");
3593        assert_eq!(
3594            contract.contract_response_type(),
3595            ContractResponseType::UpdateNotification
3596        );
3597        let notification = contract
3598            .contract_response_as_update_notification()
3599            .expect("an UpdateNotification");
3600        let related_update = notification
3601            .update()
3602            .update_data_as_related_state_update()
3603            .expect("a RelatedStateUpdate");
3604
3605        assert_eq!(
3606            related_update.related_to().data().bytes(),
3607            &INSTANCE,
3608            "related_to must be the 32 RAW id bytes. Encoding it as base58 text \
3609             puts ~44 ASCII bytes in a field the TypeScript SDK reads as a raw \
3610             Uint8Array, and that our own decoder now rejects."
3611        );
3612    }
3613
3614    /// `SecretsId::try_decode_fbs` has no production caller today, so it is
3615    /// pinned directly rather than through a request. Fixing it now means the
3616    /// first client to reach it does not find a panic waiting.
3617    #[test]
3618    fn secrets_id_wrong_length_hash_is_rejected_not_panicking() {
3619        use crate::delegate_interface::SecretsId;
3620        use crate::generated::common::{SecretsId as FbsSecretsId, SecretsIdArgs};
3621
3622        let build = |hash_len: usize| {
3623            let mut b = Builder::new();
3624            let key = b.create_vector(&[1u8, 2, 3]);
3625            let hash = b.create_vector(&vec![4u8; hash_len]);
3626            let id = FbsSecretsId::create(
3627                &mut b,
3628                &SecretsIdArgs {
3629                    key: Some(key),
3630                    hash: Some(hash),
3631                },
3632            );
3633            b.finish_minimal(id);
3634            b.finished_data().to_vec()
3635        };
3636
3637        let bytes = build(8);
3638        let fbs = flatbuffers::root::<FbsSecretsId>(&bytes)
3639            .expect("the verifier accepts a short required vector");
3640        let err = SecretsId::try_decode_fbs(&fbs).expect_err("an 8-byte hash must be rejected");
3641        assert!(
3642            err.to_string().contains("SecretsId.hash") && err.to_string().contains("got 8 bytes"),
3643            "got: {err}"
3644        );
3645
3646        let bytes = build(32);
3647        let fbs = flatbuffers::root::<FbsSecretsId>(&bytes).expect("well-formed");
3648        assert!(
3649            SecretsId::try_decode_fbs(&fbs).is_ok(),
3650            "a 32-byte hash must still decode"
3651        );
3652    }
3653}