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                    _ => {
388                        return Err(WsApiError::deserialization(
389                            "unknown client request type".to_string(),
390                        ))
391                    }
392                },
393                Err(e) => {
394                    let cause = format!("{e}");
395                    return Err(WsApiError::deserialization(cause));
396                }
397            }
398        };
399
400        Ok(req)
401    }
402}
403
404#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
405#[non_exhaustive]
406pub enum ContractRequest<'a> {
407    /// Insert a new value in a contract corresponding with the provided key.
408    Put {
409        contract: ContractContainer,
410        /// Value to upsert in the contract.
411        state: WrappedState,
412        /// Related contracts.
413        #[serde(borrow)]
414        related_contracts: RelatedContracts<'a>,
415        /// If this flag is set then subscribe to updates for this contract.
416        subscribe: bool,
417        /// If true, the PUT response waits for the subscription to complete.
418        /// Only meaningful when `subscribe` is true.
419        #[serde(default)]
420        blocking_subscribe: bool,
421    },
422    /// Update an existing contract corresponding with the provided key.
423    Update {
424        key: ContractKey,
425        #[serde(borrow)]
426        data: UpdateData<'a>,
427    },
428    /// Fetch the current state from a contract corresponding to the provided key.
429    Get {
430        /// Instance ID of the contract (the hash of code + params).
431        /// Only the instance ID is needed since the client doesn't have the code hash yet.
432        key: ContractInstanceId,
433        /// If this flag is set then fetch also the contract itself.
434        return_contract_code: bool,
435        /// If this flag is set then subscribe to updates for this contract.
436        subscribe: bool,
437        /// If true, the GET response waits for the subscription to complete.
438        /// Only meaningful when `subscribe` is true.
439        #[serde(default)]
440        blocking_subscribe: bool,
441    },
442    /// Subscribe to the changes in a given contract. Implicitly starts a get operation
443    /// if the contract is not present yet.
444    Subscribe {
445        /// Instance ID of the contract.
446        key: ContractInstanceId,
447        summary: Option<StateSummary<'a>>,
448    },
449}
450
451impl ContractRequest<'_> {
452    pub fn into_owned(self) -> ContractRequest<'static> {
453        match self {
454            Self::Put {
455                contract,
456                state,
457                related_contracts,
458                subscribe,
459                blocking_subscribe,
460            } => ContractRequest::Put {
461                contract,
462                state,
463                related_contracts: related_contracts.into_owned(),
464                subscribe,
465                blocking_subscribe,
466            },
467            Self::Update { key, data } => ContractRequest::Update {
468                key,
469                data: data.into_owned(),
470            },
471            Self::Get {
472                key,
473                return_contract_code: fetch_contract,
474                subscribe,
475                blocking_subscribe,
476            } => ContractRequest::Get {
477                key,
478                return_contract_code: fetch_contract,
479                subscribe,
480                blocking_subscribe,
481            },
482            Self::Subscribe { key, summary } => ContractRequest::Subscribe {
483                key,
484                summary: summary.map(StateSummary::into_owned),
485            },
486        }
487    }
488}
489
490impl<'a> From<ContractRequest<'a>> for ClientRequest<'a> {
491    fn from(op: ContractRequest<'a>) -> Self {
492        ClientRequest::ContractOp(op)
493    }
494}
495
496/// Deserializes a `ContractRequest` from a Flatbuffers message.
497impl<'a> TryFromFbs<&FbsContractRequest<'a>> for ContractRequest<'a> {
498    fn try_decode_fbs(request: &FbsContractRequest<'a>) -> Result<Self, WsApiError> {
499        let req = {
500            match request.contract_request_type() {
501                ContractRequestType::Get => {
502                    let get = request.contract_request_as_get().unwrap();
503                    // Extract just the instance ID - GET only needs the instance ID,
504                    // not the full key (which may not be complete on the client side)
505                    let fbs_key = get.key();
506                    let key_bytes: [u8; 32] = fbs_key.instance().data().bytes().try_into().unwrap();
507                    let key = ContractInstanceId::new(key_bytes);
508                    let fetch_contract = get.fetch_contract();
509                    let subscribe = get.subscribe();
510                    let blocking_subscribe = get.blocking_subscribe();
511                    ContractRequest::Get {
512                        key,
513                        return_contract_code: fetch_contract,
514                        subscribe,
515                        blocking_subscribe,
516                    }
517                }
518                ContractRequestType::Put => {
519                    let put = request.contract_request_as_put().unwrap();
520                    let contract = ContractContainer::try_decode_fbs(&put.container())?;
521                    let state = WrappedState::new(put.wrapped_state().bytes().to_vec());
522                    let related_contracts =
523                        RelatedContracts::try_decode_fbs(&put.related_contracts())?.into_owned();
524                    let subscribe = put.subscribe();
525                    let blocking_subscribe = put.blocking_subscribe();
526                    ContractRequest::Put {
527                        contract,
528                        state,
529                        related_contracts,
530                        subscribe,
531                        blocking_subscribe,
532                    }
533                }
534                ContractRequestType::Update => {
535                    let update = request.contract_request_as_update().unwrap();
536                    let key = ContractKey::try_decode_fbs(&update.key())?;
537                    let data = UpdateData::try_decode_fbs(&update.data())?.into_owned();
538                    ContractRequest::Update { key, data }
539                }
540                ContractRequestType::Subscribe => {
541                    let subscribe = request.contract_request_as_subscribe().unwrap();
542                    // Extract just the instance ID for Subscribe
543                    let fbs_key = subscribe.key();
544                    let key_bytes: [u8; 32] = fbs_key.instance().data().bytes().try_into().unwrap();
545                    let key = ContractInstanceId::new(key_bytes);
546                    let summary = subscribe
547                        .summary()
548                        .map(|summary_data| StateSummary::from(summary_data.bytes()));
549                    ContractRequest::Subscribe { key, summary }
550                }
551                // Reachable, not `unreachable!()`: the generated flatbuffers
552                // verifier accepts any unknown union discriminant (`_ => Ok(())`)
553                // and the union type field is a raw `u8` a client can set freely,
554                // so a crafted request reaches here. Return a per-request error
555                // instead of panicking the connection handler. (Mirrors the same
556                // fix on `DelegateRequest::try_decode_fbs`.)
557                other => {
558                    return Err(WsApiError::deserialization(format!(
559                        "unknown ContractRequestType discriminant: {}",
560                        other.0
561                    )));
562                }
563            }
564        };
565
566        Ok(req)
567    }
568}
569
570impl<'a> From<DelegateRequest<'a>> for ClientRequest<'a> {
571    fn from(op: DelegateRequest<'a>) -> Self {
572        ClientRequest::DelegateOp(op)
573    }
574}
575
576#[derive(Serialize, Deserialize, Debug, Clone)]
577#[non_exhaustive]
578pub enum DelegateRequest<'a> {
579    ApplicationMessages {
580        key: DelegateKey,
581        #[serde(deserialize_with = "Parameters::deser_params")]
582        params: Parameters<'a>,
583        #[serde(borrow)]
584        inbound: Vec<InboundDelegateMsg<'a>>,
585    },
586    RegisterDelegate {
587        delegate: DelegateContainer,
588        cipher: [u8; 32],
589        nonce: [u8; 24],
590    },
591    UnregisterDelegate(DelegateKey),
592    /// Registers a delegate AND requests a one-shot, node-side copy-forward of
593    /// the LOCAL-scope secrets belonging to `predecessors` into the newly
594    /// registered delegate's own secret namespace.
595    ///
596    /// `predecessors` are the delegate keys of retired generations of this
597    /// delegate. It is a **list** because a user may skip one or more
598    /// generations between the version they last ran and the current one, so
599    /// several prior namespaces can need forwarding in a single registration.
600    /// The copy-forward is:
601    ///   - **consent-gated on the client side** — the node performs NO
602    ///     execution of any old delegate WASM; it only copies the
603    ///     already-sealed LOCAL-scope secret bytes it holds for a predecessor
604    ///     into the successor's namespace,
605    ///   - **best-effort over unknown predecessors** — the server silently
606    ///     ignores any predecessor key it does not hold, and
607    ///   - **idempotent per `(predecessor -> successor)` pair** —
608    ///     re-registering with the same predecessors copies nothing new.
609    ///
610    /// `cipher` and `nonce` mirror [`DelegateRequest::RegisterDelegate`] purely
611    /// for field-shape parity. The node has ignored both since
612    /// freenet-core#4140 (secrets are sealed client-side), so they carry no
613    /// behavior here either; they exist only so the two registration variants
614    /// stay structurally identical.
615    RegisterDelegateWithPredecessors {
616        delegate: DelegateContainer,
617        cipher: [u8; 32],
618        nonce: [u8; 24],
619        predecessors: Vec<DelegateKey>,
620    },
621}
622
623impl DelegateRequest<'_> {
624    pub fn into_owned(self) -> DelegateRequest<'static> {
625        match self {
626            DelegateRequest::ApplicationMessages {
627                key,
628                inbound,
629                params,
630            } => DelegateRequest::ApplicationMessages {
631                key,
632                params: params.into_owned(),
633                inbound: inbound.into_iter().map(|e| e.into_owned()).collect(),
634            },
635            DelegateRequest::RegisterDelegate {
636                delegate,
637                cipher,
638                nonce,
639            } => DelegateRequest::RegisterDelegate {
640                delegate,
641                cipher,
642                nonce,
643            },
644            DelegateRequest::UnregisterDelegate(key) => DelegateRequest::UnregisterDelegate(key),
645            DelegateRequest::RegisterDelegateWithPredecessors {
646                delegate,
647                cipher,
648                nonce,
649                predecessors,
650            } => DelegateRequest::RegisterDelegateWithPredecessors {
651                delegate,
652                cipher,
653                nonce,
654                predecessors,
655            },
656        }
657    }
658
659    pub fn key(&self) -> &DelegateKey {
660        match self {
661            DelegateRequest::ApplicationMessages { key, .. } => key,
662            DelegateRequest::RegisterDelegate { delegate, .. } => delegate.key(),
663            DelegateRequest::UnregisterDelegate(key) => key,
664            DelegateRequest::RegisterDelegateWithPredecessors { delegate, .. } => delegate.key(),
665        }
666    }
667}
668
669impl Display for ClientRequest<'_> {
670    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
671        match self {
672            ClientRequest::ContractOp(op) => match op {
673                ContractRequest::Put {
674                    contract, state, ..
675                } => {
676                    write!(
677                        f,
678                        "ContractRequest::Put for contract `{contract}` with state {state}"
679                    )
680                }
681                ContractRequest::Update { key, .. } => write!(f, "update request for {key}"),
682                ContractRequest::Get {
683                    key,
684                    return_contract_code: contract,
685                    ..
686                } => {
687                    write!(
688                        f,
689                        "ContractRequest::Get for key `{key}` (fetch full contract: {contract})"
690                    )
691                }
692                ContractRequest::Subscribe { key, .. } => {
693                    write!(f, "ContractRequest::Subscribe for `{key}`")
694                }
695            },
696            ClientRequest::DelegateOp(op) => match op {
697                DelegateRequest::ApplicationMessages { key, inbound, .. } => {
698                    write!(
699                        f,
700                        "DelegateRequest::ApplicationMessages for `{key}` with {} messages",
701                        inbound.len()
702                    )
703                }
704                DelegateRequest::RegisterDelegate { delegate, .. } => {
705                    write!(
706                        f,
707                        "DelegateRequest::RegisterDelegate for delegate.key()=`{}`",
708                        delegate.key()
709                    )
710                }
711                DelegateRequest::UnregisterDelegate(key) => {
712                    write!(f, "DelegateRequest::UnregisterDelegate for key `{key}`")
713                }
714                DelegateRequest::RegisterDelegateWithPredecessors {
715                    delegate,
716                    predecessors,
717                    ..
718                } => {
719                    write!(
720                        f,
721                        "DelegateRequest::RegisterDelegateWithPredecessors for delegate.key()=`{}` with {} predecessor(s)",
722                        delegate.key(),
723                        predecessors.len()
724                    )
725                }
726            },
727            ClientRequest::Disconnect { .. } => write!(f, "client disconnected"),
728            ClientRequest::Authenticate { .. } => write!(f, "authenticate"),
729            ClientRequest::NodeQueries(query) => write!(f, "node queries: {:?}", query),
730            ClientRequest::Close => write!(f, "close"),
731            ClientRequest::StreamChunk {
732                stream_id,
733                index,
734                total,
735                ..
736            } => write!(f, "stream chunk {index}/{total} (stream {stream_id})"),
737        }
738    }
739}
740
741/// Deserializes a `DelegateRequest` from a Flatbuffers message.
742impl<'a> TryFromFbs<&FbsDelegateRequest<'a>> for DelegateRequest<'a> {
743    fn try_decode_fbs(request: &FbsDelegateRequest<'a>) -> Result<Self, WsApiError> {
744        let req = {
745            match request.delegate_request_type() {
746                DelegateRequestType::ApplicationMessages => {
747                    let app_msg = request.delegate_request_as_application_messages().unwrap();
748                    let key = DelegateKey::try_decode_fbs(&app_msg.key())?;
749                    let params = Parameters::from(app_msg.params().bytes());
750                    let inbound = app_msg
751                        .inbound()
752                        .iter()
753                        .map(|msg| InboundDelegateMsg::try_decode_fbs(&msg))
754                        .collect::<Result<Vec<_>, _>>()?;
755                    DelegateRequest::ApplicationMessages {
756                        key,
757                        params,
758                        inbound,
759                    }
760                }
761                DelegateRequestType::RegisterDelegate => {
762                    let register = request.delegate_request_as_register_delegate().unwrap();
763                    let delegate = DelegateContainer::try_decode_fbs(&register.delegate())?;
764                    let cipher =
765                        <[u8; 32]>::try_from(register.cipher().bytes().to_vec().as_slice())
766                            .unwrap();
767                    let nonce =
768                        <[u8; 24]>::try_from(register.nonce().bytes().to_vec().as_slice()).unwrap();
769                    DelegateRequest::RegisterDelegate {
770                        delegate,
771                        cipher,
772                        nonce,
773                    }
774                }
775                DelegateRequestType::UnregisterDelegate => {
776                    let unregister = request.delegate_request_as_unregister_delegate().unwrap();
777                    let key = DelegateKey::try_decode_fbs(&unregister.key())?;
778                    DelegateRequest::UnregisterDelegate(key)
779                }
780                // An unknown union discriminant is reachable, not `unreachable!()`:
781                // the generated flatbuffers verifier accepts any discriminant it
782                // doesn't recognize (`_ => Ok(())`), and the union type field is a
783                // raw `u8` the (public) TS builder can set to any value. Panicking
784                // here would let a single crafted request take down the connection
785                // handler, so return a per-request error instead.
786                other => {
787                    return Err(WsApiError::deserialization(format!(
788                        "unknown DelegateRequestType discriminant: {}",
789                        other.0
790                    )));
791                }
792            }
793        };
794
795        Ok(req)
796    }
797}
798
799/// A response to a previous [`ClientRequest`]
800#[derive(Serialize, Deserialize, Debug, Clone)]
801#[non_exhaustive]
802pub enum HostResponse<T = WrappedState> {
803    ContractResponse(#[serde(bound(deserialize = "T: DeserializeOwned"))] ContractResponse<T>),
804    DelegateResponse {
805        key: DelegateKey,
806        values: Vec<OutboundDelegateMsg>,
807    },
808    QueryResponse(QueryResponse),
809    /// A requested action which doesn't require an answer was performed successfully.
810    Ok,
811    /// A chunk of a larger streamed response.
812    StreamChunk {
813        stream_id: u32,
814        index: u32,
815        total: u32,
816        data: Bytes,
817    },
818    /// Header message announcing the start of a streamed response.
819    /// Sent before the corresponding [`StreamChunk`] messages so the client
820    /// can set up incremental consumption via [`WsStreamHandle`].
821    StreamHeader {
822        stream_id: u32,
823        total_bytes: u64,
824        content: StreamContent,
825    },
826}
827
828/// Describes what kind of response is being streamed.
829#[derive(Debug, Serialize, Deserialize, Clone)]
830pub enum StreamContent {
831    /// A streamed GetResponse — the large state is delivered via StreamChunks.
832    GetResponse {
833        key: ContractKey,
834        includes_contract: bool,
835    },
836    /// Raw binary stream (future use).
837    Raw,
838}
839
840type Peer = String;
841
842#[derive(Serialize, Deserialize, Debug, Clone)]
843pub enum QueryResponse {
844    ConnectedPeers { peers: Vec<(Peer, SocketAddr)> },
845    NetworkDebug(NetworkDebugInfo),
846    NodeDiagnostics(NodeDiagnosticsResponse),
847    NeighborHosting(NeighborHostingInfo),
848}
849
850#[derive(Serialize, Deserialize, Debug, Clone)]
851pub struct NetworkDebugInfo {
852    pub subscriptions: Vec<SubscriptionInfo>,
853    pub connected_peers: Vec<(String, SocketAddr)>,
854}
855
856#[derive(Serialize, Deserialize, Debug, Clone)]
857pub struct NodeDiagnosticsResponse {
858    /// Node information
859    pub node_info: Option<NodeInfo>,
860
861    /// Network connectivity information
862    pub network_info: Option<NetworkInfo>,
863
864    /// Contract subscription information
865    pub subscriptions: Vec<SubscriptionInfo>,
866
867    /// Contract states for specific contracts.
868    ///
869    /// Keys are the Base58-encoded contract id (i.e. `ContractKey::Display`),
870    /// matching the convention every other field in this struct uses
871    /// (`peer_id: String`, `connected_peers: Vec<(String, String)>`,
872    /// `ContractHostingEntry::contract_key: String`). Pre-0.7 this was
873    /// `HashMap<ContractKey, ContractState>`, which `serde_json` could not
874    /// serialize because JSON object keys must be strings — every report
875    /// from a node hosting at least one contract had its
876    /// `network_status` silently dropped. See freenet/freenet-core#3987.
877    pub contract_states: std::collections::HashMap<String, ContractState>,
878
879    /// System metrics
880    pub system_metrics: Option<SystemMetrics>,
881
882    /// Information about connected peers with detailed data
883    pub connected_peers_detailed: Vec<ConnectedPeerInfo>,
884}
885
886#[derive(Serialize, Deserialize, Debug, Clone)]
887pub struct NodeInfo {
888    pub peer_id: String,
889    pub is_gateway: bool,
890    pub location: Option<String>,
891    pub listening_address: Option<String>,
892    pub uptime_seconds: u64,
893}
894
895#[derive(Serialize, Deserialize, Debug, Clone)]
896pub struct NetworkInfo {
897    pub connected_peers: Vec<(String, String)>, // (peer_id, address)
898    pub active_connections: usize,
899}
900
901#[derive(Serialize, Deserialize, Debug, Clone)]
902pub struct ContractState {
903    /// Number of nodes subscribed to this contract
904    pub subscribers: u32,
905    /// Peer IDs of nodes that are subscribed to this contract
906    pub subscriber_peer_ids: Vec<String>,
907    /// Size of the contract state in bytes
908    #[serde(default)]
909    pub size_bytes: u64,
910}
911
912#[derive(Serialize, Deserialize, Debug, Clone)]
913pub struct SystemMetrics {
914    pub active_connections: u32,
915    pub hosting_contracts: u32,
916}
917
918#[derive(Serialize, Deserialize, Debug, Clone)]
919pub struct SubscriptionInfo {
920    pub contract_key: ContractInstanceId,
921    pub client_id: usize,
922}
923
924/// Basic information about a connected peer
925#[derive(Serialize, Deserialize, Debug, Clone)]
926pub struct ConnectedPeerInfo {
927    pub peer_id: String,
928    pub address: String,
929}
930
931#[derive(Serialize, Deserialize, Debug, Clone)]
932pub enum NodeQuery {
933    ConnectedPeers,
934    SubscriptionInfo,
935    NodeDiagnostics {
936        /// Diagnostic configuration specifying what information to collect
937        config: NodeDiagnosticsConfig,
938    },
939    /// Query neighbor hosting information for update propagation
940    NeighborHostingInfo,
941}
942
943/// Neighbor hosting information for update propagation
944#[derive(Serialize, Deserialize, Debug, Clone)]
945pub struct NeighborHostingInfo {
946    /// Contracts this node is currently hosting
947    pub my_hosted: Vec<ContractHostingEntry>,
948    /// What we know about neighbor hosting
949    pub neighbor_hosting: Vec<NeighborHostingDetail>,
950    /// Hosting propagation statistics
951    pub stats: HostingStats,
952}
953
954#[derive(Serialize, Deserialize, Debug, Clone)]
955pub struct ContractHostingEntry {
956    /// Full contract key as string
957    pub contract_key: String,
958    /// 32-bit hash for proximity matching
959    pub hosting_hash: u32,
960    /// When this contract was first hosted (Unix timestamp)
961    pub hosted_since: u64,
962}
963
964#[derive(Serialize, Deserialize, Debug, Clone)]
965pub struct NeighborHostingDetail {
966    /// Peer identifier
967    pub peer_id: String,
968    /// Contract hashes this neighbor is known to host
969    pub known_contracts: Vec<u32>,
970    /// Last update received from this neighbor (Unix timestamp)
971    pub last_update: u64,
972    /// Number of updates received from this neighbor
973    pub update_count: u64,
974}
975
976#[derive(Serialize, Deserialize, Debug, Clone)]
977pub struct HostingStats {
978    /// Number of hosting announcements sent
979    pub hosting_announces_sent: u64,
980    /// Number of hosting announcements received
981    pub hosting_announces_received: u64,
982    /// Updates forwarded via proximity (not subscription)
983    pub updates_via_proximity: u64,
984    /// Updates forwarded via subscription
985    pub updates_via_subscription: u64,
986    /// False positives due to hash collisions
987    pub false_positive_forwards: u64,
988    /// Average number of contracts per neighbor
989    pub avg_neighbor_hosting_size: f32,
990}
991
992#[derive(Serialize, Deserialize, Debug, Clone)]
993pub struct NodeDiagnosticsConfig {
994    /// Include basic node information (ID, location, uptime, etc.)
995    pub include_node_info: bool,
996
997    /// Include network connectivity information
998    pub include_network_info: bool,
999
1000    /// Include contract subscription information
1001    pub include_subscriptions: bool,
1002
1003    /// Include contract states for specific contracts (empty = all contracts)
1004    pub contract_keys: Vec<ContractKey>,
1005
1006    /// Include memory and performance metrics
1007    pub include_system_metrics: bool,
1008
1009    /// Include detailed information about connected peers (vs basic peer list)
1010    pub include_detailed_peer_info: bool,
1011
1012    /// Include peer IDs of subscribers in contract state information
1013    pub include_subscriber_peer_ids: bool,
1014}
1015
1016impl NodeDiagnosticsConfig {
1017    /// Create a comprehensive diagnostic config for debugging update propagation issues
1018    pub fn for_update_propagation_debugging(contract_key: ContractKey) -> Self {
1019        Self {
1020            include_node_info: true,
1021            include_network_info: true,
1022            include_subscriptions: true,
1023            contract_keys: vec![contract_key],
1024            include_system_metrics: true,
1025            include_detailed_peer_info: true,
1026            include_subscriber_peer_ids: true,
1027        }
1028    }
1029
1030    /// Create a lightweight diagnostic config for basic node status
1031    pub fn basic_status() -> Self {
1032        Self {
1033            include_node_info: true,
1034            include_network_info: true,
1035            include_subscriptions: false,
1036            contract_keys: vec![],
1037            include_system_metrics: false,
1038            include_detailed_peer_info: false,
1039            include_subscriber_peer_ids: false,
1040        }
1041    }
1042
1043    /// Create a full diagnostic config (all information)
1044    pub fn full() -> Self {
1045        Self {
1046            include_node_info: true,
1047            include_network_info: true,
1048            include_subscriptions: true,
1049            contract_keys: vec![], // empty = all contracts
1050            include_system_metrics: true,
1051            include_detailed_peer_info: true,
1052            include_subscriber_peer_ids: true,
1053        }
1054    }
1055}
1056
1057impl HostResponse {
1058    pub fn unwrap_put(self) -> ContractKey {
1059        if let Self::ContractResponse(ContractResponse::PutResponse { key }) = self {
1060            key
1061        } else {
1062            panic!("called `HostResponse::unwrap_put()` on other than `PutResponse` value")
1063        }
1064    }
1065
1066    pub fn unwrap_get(self) -> (WrappedState, Option<ContractContainer>) {
1067        if let Self::ContractResponse(ContractResponse::GetResponse {
1068            contract, state, ..
1069        }) = self
1070        {
1071            (state, contract)
1072        } else {
1073            panic!("called `HostResponse::unwrap_put()` on other than `PutResponse` value")
1074        }
1075    }
1076
1077    pub fn into_fbs_bytes(self) -> Result<Vec<u8>, Box<ClientError>> {
1078        let mut builder = flatbuffers::FlatBufferBuilder::new();
1079        match self {
1080            HostResponse::ContractResponse(res) => match res {
1081                ContractResponse::PutResponse { key } => {
1082                    let instance_data = builder.create_vector(key.as_bytes());
1083                    let instance_offset = FbsContractInstanceId::create(
1084                        &mut builder,
1085                        &ContractInstanceIdArgs {
1086                            data: Some(instance_data),
1087                        },
1088                    );
1089
1090                    let code = Some(builder.create_vector(&key.code_hash().0));
1091                    let key_offset = FbsContractKey::create(
1092                        &mut builder,
1093                        &ContractKeyArgs {
1094                            instance: Some(instance_offset),
1095                            code,
1096                        },
1097                    );
1098
1099                    let put_offset = FbsPutResponse::create(
1100                        &mut builder,
1101                        &PutResponseArgs {
1102                            key: Some(key_offset),
1103                        },
1104                    );
1105
1106                    let contract_response_offset = FbsContractResponse::create(
1107                        &mut builder,
1108                        &ContractResponseArgs {
1109                            contract_response: Some(put_offset.as_union_value()),
1110                            contract_response_type: ContractResponseType::PutResponse,
1111                        },
1112                    );
1113
1114                    let response_offset = FbsHostResponse::create(
1115                        &mut builder,
1116                        &HostResponseArgs {
1117                            response: Some(contract_response_offset.as_union_value()),
1118                            response_type: HostResponseType::ContractResponse,
1119                        },
1120                    );
1121
1122                    finish_host_response_buffer(&mut builder, response_offset);
1123                    Ok(builder.finished_data().to_vec())
1124                }
1125                ContractResponse::UpdateResponse { key, summary } => {
1126                    let instance_data = builder.create_vector(key.as_bytes());
1127                    let instance_offset = FbsContractInstanceId::create(
1128                        &mut builder,
1129                        &ContractInstanceIdArgs {
1130                            data: Some(instance_data),
1131                        },
1132                    );
1133
1134                    let code = Some(builder.create_vector(&key.code_hash().0));
1135
1136                    let key_offset = FbsContractKey::create(
1137                        &mut builder,
1138                        &ContractKeyArgs {
1139                            instance: Some(instance_offset),
1140                            code,
1141                        },
1142                    );
1143
1144                    let summary_data = builder.create_vector(&summary.into_bytes());
1145
1146                    let update_response_offset = FbsUpdateResponse::create(
1147                        &mut builder,
1148                        &UpdateResponseArgs {
1149                            key: Some(key_offset),
1150                            summary: Some(summary_data),
1151                        },
1152                    );
1153
1154                    let contract_response_offset = FbsContractResponse::create(
1155                        &mut builder,
1156                        &ContractResponseArgs {
1157                            contract_response: Some(update_response_offset.as_union_value()),
1158                            contract_response_type: ContractResponseType::UpdateResponse,
1159                        },
1160                    );
1161
1162                    let response_offset = FbsHostResponse::create(
1163                        &mut builder,
1164                        &HostResponseArgs {
1165                            response: Some(contract_response_offset.as_union_value()),
1166                            response_type: HostResponseType::ContractResponse,
1167                        },
1168                    );
1169
1170                    finish_host_response_buffer(&mut builder, response_offset);
1171                    Ok(builder.finished_data().to_vec())
1172                }
1173                ContractResponse::GetResponse {
1174                    key,
1175                    contract: contract_container,
1176                    state,
1177                } => {
1178                    let instance_data = builder.create_vector(key.as_bytes());
1179                    let instance_offset = FbsContractInstanceId::create(
1180                        &mut builder,
1181                        &ContractInstanceIdArgs {
1182                            data: Some(instance_data),
1183                        },
1184                    );
1185
1186                    let code = Some(builder.create_vector(&key.code_hash().0));
1187                    let key_offset = FbsContractKey::create(
1188                        &mut builder,
1189                        &ContractKeyArgs {
1190                            instance: Some(instance_offset),
1191                            code,
1192                        },
1193                    );
1194
1195                    let container_offset = if let Some(contract) = contract_container {
1196                        let data = builder.create_vector(contract.key().as_bytes());
1197
1198                        let instance_offset = FbsContractInstanceId::create(
1199                            &mut builder,
1200                            &ContractInstanceIdArgs { data: Some(data) },
1201                        );
1202
1203                        let code = Some(builder.create_vector(&contract.key().code_hash().0));
1204                        let contract_key_offset = FbsContractKey::create(
1205                            &mut builder,
1206                            &ContractKeyArgs {
1207                                instance: Some(instance_offset),
1208                                code,
1209                            },
1210                        );
1211
1212                        let contract_data =
1213                            builder.create_vector(contract.clone().unwrap_v1().data.data());
1214                        let contract_code_hash =
1215                            builder.create_vector(&contract.clone().unwrap_v1().data.hash().0);
1216
1217                        let contract_code_offset = ContractCode::create(
1218                            &mut builder,
1219                            &ContractCodeArgs {
1220                                data: Some(contract_data),
1221                                code_hash: Some(contract_code_hash),
1222                            },
1223                        );
1224
1225                        let contract_params =
1226                            builder.create_vector(&contract.clone().params().into_bytes());
1227
1228                        let contract_offset = match contract {
1229                            Wasm(V1(..)) => WasmContractV1::create(
1230                                &mut builder,
1231                                &WasmContractV1Args {
1232                                    key: Some(contract_key_offset),
1233                                    data: Some(contract_code_offset),
1234                                    parameters: Some(contract_params),
1235                                },
1236                            ),
1237                        };
1238
1239                        Some(FbsContractContainer::create(
1240                            &mut builder,
1241                            &ContractContainerArgs {
1242                                contract_type: ContractType::WasmContractV1,
1243                                contract: Some(contract_offset.as_union_value()),
1244                            },
1245                        ))
1246                    } else {
1247                        None
1248                    };
1249
1250                    let state_data = builder.create_vector(&state);
1251
1252                    let get_offset = FbsGetResponse::create(
1253                        &mut builder,
1254                        &GetResponseArgs {
1255                            key: Some(key_offset),
1256                            contract: container_offset,
1257                            state: Some(state_data),
1258                        },
1259                    );
1260
1261                    let contract_response_offset = FbsContractResponse::create(
1262                        &mut builder,
1263                        &ContractResponseArgs {
1264                            contract_response_type: ContractResponseType::GetResponse,
1265                            contract_response: Some(get_offset.as_union_value()),
1266                        },
1267                    );
1268
1269                    let response_offset = FbsHostResponse::create(
1270                        &mut builder,
1271                        &HostResponseArgs {
1272                            response: Some(contract_response_offset.as_union_value()),
1273                            response_type: HostResponseType::ContractResponse,
1274                        },
1275                    );
1276
1277                    finish_host_response_buffer(&mut builder, response_offset);
1278                    Ok(builder.finished_data().to_vec())
1279                }
1280                ContractResponse::UpdateNotification { key, update } => {
1281                    let instance_data = builder.create_vector(key.as_bytes());
1282                    let instance_offset = FbsContractInstanceId::create(
1283                        &mut builder,
1284                        &ContractInstanceIdArgs {
1285                            data: Some(instance_data),
1286                        },
1287                    );
1288
1289                    let code = Some(builder.create_vector(&key.code_hash().0));
1290                    let key_offset = FbsContractKey::create(
1291                        &mut builder,
1292                        &ContractKeyArgs {
1293                            instance: Some(instance_offset),
1294                            code,
1295                        },
1296                    );
1297
1298                    let update_data = match update {
1299                        State(state) => {
1300                            let state_data = builder.create_vector(&state.into_bytes());
1301                            let state_update_offset = StateUpdate::create(
1302                                &mut builder,
1303                                &StateUpdateArgs {
1304                                    state: Some(state_data),
1305                                },
1306                            );
1307                            FbsUpdateData::create(
1308                                &mut builder,
1309                                &UpdateDataArgs {
1310                                    update_data_type: UpdateDataType::StateUpdate,
1311                                    update_data: Some(state_update_offset.as_union_value()),
1312                                },
1313                            )
1314                        }
1315                        Delta(delta) => {
1316                            let delta_data = builder.create_vector(&delta.into_bytes());
1317                            let update_offset = DeltaUpdate::create(
1318                                &mut builder,
1319                                &DeltaUpdateArgs {
1320                                    delta: Some(delta_data),
1321                                },
1322                            );
1323                            FbsUpdateData::create(
1324                                &mut builder,
1325                                &UpdateDataArgs {
1326                                    update_data_type: UpdateDataType::DeltaUpdate,
1327                                    update_data: Some(update_offset.as_union_value()),
1328                                },
1329                            )
1330                        }
1331                        StateAndDelta { state, delta } => {
1332                            let state_data = builder.create_vector(&state.into_bytes());
1333                            let delta_data = builder.create_vector(&delta.into_bytes());
1334
1335                            let update_offset = StateAndDeltaUpdate::create(
1336                                &mut builder,
1337                                &StateAndDeltaUpdateArgs {
1338                                    state: Some(state_data),
1339                                    delta: Some(delta_data),
1340                                },
1341                            );
1342
1343                            FbsUpdateData::create(
1344                                &mut builder,
1345                                &UpdateDataArgs {
1346                                    update_data_type: UpdateDataType::StateAndDeltaUpdate,
1347                                    update_data: Some(update_offset.as_union_value()),
1348                                },
1349                            )
1350                        }
1351                        RelatedState { related_to, state } => {
1352                            let state_data = builder.create_vector(&state.into_bytes());
1353                            let instance_data =
1354                                builder.create_vector(related_to.encode().as_bytes());
1355
1356                            let instance_offset = FbsContractInstanceId::create(
1357                                &mut builder,
1358                                &ContractInstanceIdArgs {
1359                                    data: Some(instance_data),
1360                                },
1361                            );
1362
1363                            let update_offset = RelatedStateUpdate::create(
1364                                &mut builder,
1365                                &RelatedStateUpdateArgs {
1366                                    related_to: Some(instance_offset),
1367                                    state: Some(state_data),
1368                                },
1369                            );
1370
1371                            FbsUpdateData::create(
1372                                &mut builder,
1373                                &UpdateDataArgs {
1374                                    update_data_type: UpdateDataType::RelatedStateUpdate,
1375                                    update_data: Some(update_offset.as_union_value()),
1376                                },
1377                            )
1378                        }
1379                        RelatedDelta { related_to, delta } => {
1380                            let instance_data =
1381                                builder.create_vector(related_to.encode().as_bytes());
1382                            let delta_data = builder.create_vector(&delta.into_bytes());
1383
1384                            let instance_offset = FbsContractInstanceId::create(
1385                                &mut builder,
1386                                &ContractInstanceIdArgs {
1387                                    data: Some(instance_data),
1388                                },
1389                            );
1390
1391                            let update_offset = RelatedDeltaUpdate::create(
1392                                &mut builder,
1393                                &RelatedDeltaUpdateArgs {
1394                                    related_to: Some(instance_offset),
1395                                    delta: Some(delta_data),
1396                                },
1397                            );
1398
1399                            FbsUpdateData::create(
1400                                &mut builder,
1401                                &UpdateDataArgs {
1402                                    update_data_type: UpdateDataType::RelatedDeltaUpdate,
1403                                    update_data: Some(update_offset.as_union_value()),
1404                                },
1405                            )
1406                        }
1407                        RelatedStateAndDelta {
1408                            related_to,
1409                            state,
1410                            delta,
1411                        } => {
1412                            let instance_data =
1413                                builder.create_vector(related_to.encode().as_bytes());
1414                            let state_data = builder.create_vector(&state.into_bytes());
1415                            let delta_data = builder.create_vector(&delta.into_bytes());
1416
1417                            let instance_offset = FbsContractInstanceId::create(
1418                                &mut builder,
1419                                &ContractInstanceIdArgs {
1420                                    data: Some(instance_data),
1421                                },
1422                            );
1423
1424                            let update_offset = RelatedStateAndDeltaUpdate::create(
1425                                &mut builder,
1426                                &RelatedStateAndDeltaUpdateArgs {
1427                                    related_to: Some(instance_offset),
1428                                    state: Some(state_data),
1429                                    delta: Some(delta_data),
1430                                },
1431                            );
1432
1433                            FbsUpdateData::create(
1434                                &mut builder,
1435                                &UpdateDataArgs {
1436                                    update_data_type: UpdateDataType::RelatedStateAndDeltaUpdate,
1437                                    update_data: Some(update_offset.as_union_value()),
1438                                },
1439                            )
1440                        }
1441                    };
1442
1443                    let update_notification_offset = FbsUpdateNotification::create(
1444                        &mut builder,
1445                        &UpdateNotificationArgs {
1446                            key: Some(key_offset),
1447                            update: Some(update_data),
1448                        },
1449                    );
1450
1451                    let put_response_offset = FbsContractResponse::create(
1452                        &mut builder,
1453                        &ContractResponseArgs {
1454                            contract_response_type: ContractResponseType::UpdateNotification,
1455                            contract_response: Some(update_notification_offset.as_union_value()),
1456                        },
1457                    );
1458
1459                    let host_response_offset = FbsHostResponse::create(
1460                        &mut builder,
1461                        &HostResponseArgs {
1462                            response_type: HostResponseType::ContractResponse,
1463                            response: Some(put_response_offset.as_union_value()),
1464                        },
1465                    );
1466
1467                    finish_host_response_buffer(&mut builder, host_response_offset);
1468                    Ok(builder.finished_data().to_vec())
1469                }
1470                ContractResponse::SubscribeResponse { key, .. } => {
1471                    // SubscribeResponse FBS type not yet in generated code,
1472                    // serialize as PutResponse (same shape: just a key) so
1473                    // the client receives a valid response instead of a crash.
1474                    let instance_data = builder.create_vector(key.as_bytes());
1475                    let instance_offset = FbsContractInstanceId::create(
1476                        &mut builder,
1477                        &ContractInstanceIdArgs {
1478                            data: Some(instance_data),
1479                        },
1480                    );
1481                    let code = Some(builder.create_vector(&key.code_hash().0));
1482                    let key_offset = FbsContractKey::create(
1483                        &mut builder,
1484                        &ContractKeyArgs {
1485                            instance: Some(instance_offset),
1486                            code,
1487                        },
1488                    );
1489                    let put_offset = FbsPutResponse::create(
1490                        &mut builder,
1491                        &PutResponseArgs {
1492                            key: Some(key_offset),
1493                        },
1494                    );
1495                    let contract_response_offset = FbsContractResponse::create(
1496                        &mut builder,
1497                        &ContractResponseArgs {
1498                            contract_response_type: ContractResponseType::PutResponse,
1499                            contract_response: Some(put_offset.as_union_value()),
1500                        },
1501                    );
1502                    let host_response_offset = FbsHostResponse::create(
1503                        &mut builder,
1504                        &HostResponseArgs {
1505                            response_type: HostResponseType::ContractResponse,
1506                            response: Some(contract_response_offset.as_union_value()),
1507                        },
1508                    );
1509                    finish_host_response_buffer(&mut builder, host_response_offset);
1510                    Ok(builder.finished_data().to_vec())
1511                }
1512                ContractResponse::NotFound { instance_id } => {
1513                    let instance_data = builder.create_vector(instance_id.as_bytes());
1514                    let instance_offset = FbsContractInstanceId::create(
1515                        &mut builder,
1516                        &ContractInstanceIdArgs {
1517                            data: Some(instance_data),
1518                        },
1519                    );
1520
1521                    let not_found_offset = FbsNotFound::create(
1522                        &mut builder,
1523                        &NotFoundArgs {
1524                            instance_id: Some(instance_offset),
1525                        },
1526                    );
1527
1528                    let contract_response_offset = FbsContractResponse::create(
1529                        &mut builder,
1530                        &ContractResponseArgs {
1531                            contract_response_type: ContractResponseType::NotFound,
1532                            contract_response: Some(not_found_offset.as_union_value()),
1533                        },
1534                    );
1535
1536                    let response_offset = FbsHostResponse::create(
1537                        &mut builder,
1538                        &HostResponseArgs {
1539                            response: Some(contract_response_offset.as_union_value()),
1540                            response_type: HostResponseType::ContractResponse,
1541                        },
1542                    );
1543
1544                    finish_host_response_buffer(&mut builder, response_offset);
1545                    Ok(builder.finished_data().to_vec())
1546                }
1547            },
1548            HostResponse::DelegateResponse { key, values } => {
1549                let key_data = builder.create_vector(key.bytes());
1550                let code_hash_data = builder.create_vector(&key.code_hash().0);
1551                let key_offset = FbsDelegateKey::create(
1552                    &mut builder,
1553                    &DelegateKeyArgs {
1554                        key: Some(key_data),
1555                        code_hash: Some(code_hash_data),
1556                    },
1557                );
1558                let mut messages: Vec<WIPOffset<FbsOutboundDelegateMsg>> = Vec::new();
1559                values.iter().for_each(|msg| match msg {
1560                    OutboundDelegateMsg::ApplicationMessage(app) => {
1561                        let payload_data = builder.create_vector(&app.payload);
1562                        let delegate_context_data = builder.create_vector(app.context.as_ref());
1563                        let app_offset = FbsApplicationMessage::create(
1564                            &mut builder,
1565                            &ApplicationMessageArgs {
1566                                payload: Some(payload_data),
1567                                context: Some(delegate_context_data),
1568                                processed: app.processed,
1569                            },
1570                        );
1571                        let msg = FbsOutboundDelegateMsg::create(
1572                            &mut builder,
1573                            &OutboundDelegateMsgArgs {
1574                                inbound_type: OutboundDelegateMsgType::common_ApplicationMessage,
1575                                inbound: Some(app_offset.as_union_value()),
1576                            },
1577                        );
1578                        messages.push(msg);
1579                    }
1580                    OutboundDelegateMsg::RequestUserInput(input) => {
1581                        let message_data = builder.create_vector(input.message.bytes());
1582                        let mut responses: Vec<WIPOffset<FbsClientResponse>> = Vec::new();
1583                        input.responses.iter().for_each(|resp| {
1584                            let response_data = builder.create_vector(resp.bytes());
1585                            let response = FbsClientResponse::create(
1586                                &mut builder,
1587                                &ClientResponseArgs {
1588                                    data: Some(response_data),
1589                                },
1590                            );
1591                            responses.push(response)
1592                        });
1593                        let responses_offset = builder.create_vector(&responses);
1594                        let input_offset = FbsRequestUserInput::create(
1595                            &mut builder,
1596                            &RequestUserInputArgs {
1597                                request_id: input.request_id,
1598                                message: Some(message_data),
1599                                responses: Some(responses_offset),
1600                            },
1601                        );
1602                        let msg = FbsOutboundDelegateMsg::create(
1603                            &mut builder,
1604                            &OutboundDelegateMsgArgs {
1605                                inbound_type: OutboundDelegateMsgType::RequestUserInput,
1606                                inbound: Some(input_offset.as_union_value()),
1607                            },
1608                        );
1609                        messages.push(msg);
1610                    }
1611                    OutboundDelegateMsg::ContextUpdated(context) => {
1612                        let context_data = builder.create_vector(context.as_ref());
1613                        let context_offset = FbsContextUpdated::create(
1614                            &mut builder,
1615                            &ContextUpdatedArgs {
1616                                context: Some(context_data),
1617                            },
1618                        );
1619                        let msg = FbsOutboundDelegateMsg::create(
1620                            &mut builder,
1621                            &OutboundDelegateMsgArgs {
1622                                inbound_type: OutboundDelegateMsgType::ContextUpdated,
1623                                inbound: Some(context_offset.as_union_value()),
1624                            },
1625                        );
1626                        messages.push(msg);
1627                    }
1628                    OutboundDelegateMsg::GetContractRequest(_) => {
1629                        // GetContractRequest should be handled by the executor and never
1630                        // reach client serialization. If we get here, it's a bug.
1631                        tracing::error!(
1632                            "GetContractRequest reached client serialization - this is a bug"
1633                        );
1634                    }
1635                    OutboundDelegateMsg::PutContractRequest(_) => {
1636                        // PutContractRequest should be handled by the executor and never
1637                        // reach client serialization. If we get here, it's a bug.
1638                        tracing::error!(
1639                            "PutContractRequest reached client serialization - this is a bug"
1640                        );
1641                    }
1642                    OutboundDelegateMsg::UpdateContractRequest(_) => {
1643                        tracing::error!(
1644                            "UpdateContractRequest reached client serialization - this is a bug"
1645                        );
1646                    }
1647                    OutboundDelegateMsg::SubscribeContractRequest(_) => {
1648                        tracing::error!(
1649                            "SubscribeContractRequest reached client serialization - this is a bug"
1650                        );
1651                    }
1652                    OutboundDelegateMsg::SendDelegateMessage(_) => {
1653                        tracing::error!(
1654                            "SendDelegateMessage reached client serialization - this is a bug"
1655                        );
1656                    }
1657                });
1658                let messages_offset = builder.create_vector(&messages);
1659                let delegate_response_offset = FbsDelegateResponse::create(
1660                    &mut builder,
1661                    &DelegateResponseArgs {
1662                        key: Some(key_offset),
1663                        values: Some(messages_offset),
1664                    },
1665                );
1666                let host_response_offset = FbsHostResponse::create(
1667                    &mut builder,
1668                    &HostResponseArgs {
1669                        response_type: HostResponseType::DelegateResponse,
1670                        response: Some(delegate_response_offset.as_union_value()),
1671                    },
1672                );
1673                finish_host_response_buffer(&mut builder, host_response_offset);
1674                Ok(builder.finished_data().to_vec())
1675            }
1676            HostResponse::Ok => {
1677                let ok_offset = FbsOk::create(&mut builder, &OkArgs { msg: None });
1678                let host_response_offset = FbsHostResponse::create(
1679                    &mut builder,
1680                    &HostResponseArgs {
1681                        response_type: HostResponseType::Ok,
1682                        response: Some(ok_offset.as_union_value()),
1683                    },
1684                );
1685                finish_host_response_buffer(&mut builder, host_response_offset);
1686                Ok(builder.finished_data().to_vec())
1687            }
1688            HostResponse::QueryResponse(_) => unimplemented!(),
1689            HostResponse::StreamChunk {
1690                stream_id,
1691                index,
1692                total,
1693                data,
1694            } => {
1695                let data_offset = builder.create_vector(&data);
1696                let chunk_offset = FbsHostStreamChunk::create(
1697                    &mut builder,
1698                    &FbsHostStreamChunkArgs {
1699                        stream_id,
1700                        index,
1701                        total,
1702                        data: Some(data_offset),
1703                    },
1704                );
1705                let host_response_offset = FbsHostResponse::create(
1706                    &mut builder,
1707                    &HostResponseArgs {
1708                        response_type: HostResponseType::StreamChunk,
1709                        response: Some(chunk_offset.as_union_value()),
1710                    },
1711                );
1712                finish_host_response_buffer(&mut builder, host_response_offset);
1713                Ok(builder.finished_data().to_vec())
1714            }
1715            HostResponse::StreamHeader { .. } => {
1716                // StreamHeader is only sent over bincode (Native encoding) to
1717                // streaming-capable clients. Flatbuffers clients use transparent
1718                // reassembly via StreamChunk only.
1719                Err(Box::new(ClientError::from(ErrorKind::Unhandled {
1720                    cause: "StreamHeader is not supported over flatbuffers encoding".into(),
1721                })))
1722            }
1723        }
1724    }
1725}
1726
1727impl Display for HostResponse {
1728    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1729        match self {
1730            HostResponse::ContractResponse(res) => match res {
1731                ContractResponse::PutResponse { key } => {
1732                    f.write_fmt(format_args!("put response for `{key}`"))
1733                }
1734                ContractResponse::UpdateResponse { key, .. } => {
1735                    f.write_fmt(format_args!("update response for `{key}`"))
1736                }
1737                ContractResponse::GetResponse { key, .. } => {
1738                    f.write_fmt(format_args!("get response for `{key}`"))
1739                }
1740                ContractResponse::UpdateNotification { key, .. } => {
1741                    f.write_fmt(format_args!("update notification for `{key}`"))
1742                }
1743                ContractResponse::SubscribeResponse { key, .. } => {
1744                    f.write_fmt(format_args!("subscribe response for `{key}`"))
1745                }
1746                ContractResponse::NotFound { instance_id } => {
1747                    f.write_fmt(format_args!("not found for `{instance_id}`"))
1748                }
1749            },
1750            HostResponse::DelegateResponse { .. } => write!(f, "delegate responses"),
1751            HostResponse::Ok => write!(f, "ok response"),
1752            HostResponse::QueryResponse(_) => write!(f, "query response"),
1753            HostResponse::StreamChunk {
1754                stream_id,
1755                index,
1756                total,
1757                ..
1758            } => write!(f, "stream chunk {index}/{total} (stream {stream_id})"),
1759            HostResponse::StreamHeader {
1760                stream_id,
1761                total_bytes,
1762                ..
1763            } => write!(f, "stream header (stream {stream_id}, {total_bytes} bytes)"),
1764        }
1765    }
1766}
1767
1768#[derive(Clone, Serialize, Deserialize, Debug)]
1769#[non_exhaustive]
1770pub enum ContractResponse<T = WrappedState> {
1771    GetResponse {
1772        key: ContractKey,
1773        contract: Option<ContractContainer>,
1774        #[serde(bound(deserialize = "T: DeserializeOwned"))]
1775        state: T,
1776    },
1777    PutResponse {
1778        key: ContractKey,
1779    },
1780    /// Message sent when there is an update to a subscribed contract.
1781    UpdateNotification {
1782        key: ContractKey,
1783        #[serde(deserialize_with = "UpdateData::deser_update_data")]
1784        update: UpdateData<'static>,
1785    },
1786    /// Successful update
1787    UpdateResponse {
1788        key: ContractKey,
1789        #[serde(deserialize_with = "StateSummary::deser_state_summary")]
1790        summary: StateSummary<'static>,
1791    },
1792    SubscribeResponse {
1793        key: ContractKey,
1794        subscribed: bool,
1795    },
1796    /// Contract was not found after exhaustive search.
1797    /// This is an explicit response that distinguishes "contract doesn't exist"
1798    /// from other failure modes like timeouts or network errors.
1799    NotFound {
1800        /// The instance ID that was searched for.
1801        instance_id: ContractInstanceId,
1802    },
1803}
1804
1805impl<T> From<ContractResponse<T>> for HostResponse<T> {
1806    fn from(value: ContractResponse<T>) -> HostResponse<T> {
1807        HostResponse::ContractResponse(value)
1808    }
1809}
1810
1811#[cfg(test)]
1812mod node_diagnostics_response_tests {
1813    use super::{
1814        ConnectedPeerInfo, ContractState, NetworkInfo, NodeDiagnosticsResponse, NodeInfo,
1815        SubscriptionInfo, SystemMetrics,
1816    };
1817    use crate::contract_interface::ContractInstanceId;
1818    use std::collections::HashMap;
1819
1820    /// Regression for freenet/freenet-core#3987.
1821    ///
1822    /// Pre-0.7 `contract_states` was `HashMap<ContractKey, ContractState>`.
1823    /// `ContractKey` derives `Serialize` as a struct (`{instance, code}`),
1824    /// which `serde_json` rejects with `key must be a string` because JSON
1825    /// object keys must be strings. The wire path between core and clients
1826    /// is bincode (which doesn't care about key types), so the bug stayed
1827    /// invisible until the `freenet service report` binary tried to
1828    /// JSON-serialize the response for upload — every report from a node
1829    /// hosting at least one contract uploaded with empty `network_status`.
1830    ///
1831    /// All six fields are populated so that any future `pub` field added
1832    /// to the struct gets exercised by serde_json the moment a contributor
1833    /// sets a non-default value here. If a future field reintroduces the
1834    /// non-string-key pattern (e.g. `HashMap<PeerId, _>`), this test will
1835    /// fail at the source instead of silently breaking the report path.
1836    #[test]
1837    fn node_diagnostics_response_json_round_trips() {
1838        let mut contract_states = HashMap::new();
1839        contract_states.insert(
1840            "6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9".to_string(),
1841            ContractState {
1842                subscribers: 3,
1843                subscriber_peer_ids: vec!["peer-a".to_string(), "peer-b".to_string()],
1844                size_bytes: 1024,
1845            },
1846        );
1847
1848        let response = NodeDiagnosticsResponse {
1849            node_info: Some(NodeInfo {
1850                peer_id: "peer-self".to_string(),
1851                is_gateway: true,
1852                location: Some("0.5".to_string()),
1853                listening_address: Some("0.0.0.0:31337".to_string()),
1854                uptime_seconds: 3600,
1855            }),
1856            network_info: Some(NetworkInfo {
1857                connected_peers: vec![("peer-x".to_string(), "10.0.0.1:31337".to_string())],
1858                active_connections: 1,
1859            }),
1860            subscriptions: vec![SubscriptionInfo {
1861                contract_key: ContractInstanceId::new([7u8; 32]),
1862                client_id: 42,
1863            }],
1864            contract_states,
1865            system_metrics: Some(SystemMetrics {
1866                active_connections: 1,
1867                hosting_contracts: 1,
1868            }),
1869            connected_peers_detailed: vec![ConnectedPeerInfo {
1870                peer_id: "peer-x".to_string(),
1871                address: "10.0.0.1:31337".to_string(),
1872            }],
1873        };
1874
1875        let json = serde_json::to_string(&response).expect("must serialize to JSON");
1876        let parsed: serde_json::Value = serde_json::from_str(&json).expect("output is valid JSON");
1877
1878        // Every top-level field is present and distinguishable.
1879        let obj = parsed.as_object().expect("top-level must be object");
1880        assert_eq!(obj.len(), 6, "expected six top-level fields, got {obj:?}");
1881        assert_eq!(parsed["node_info"]["peer_id"], "peer-self");
1882        assert_eq!(parsed["network_info"]["active_connections"], 1);
1883        assert_eq!(parsed["subscriptions"][0]["client_id"], 42);
1884        assert_eq!(parsed["system_metrics"]["hosting_contracts"], 1);
1885        assert_eq!(parsed["connected_peers_detailed"][0]["peer_id"], "peer-x");
1886
1887        let states = parsed["contract_states"]
1888            .as_object()
1889            .expect("contract_states must be a JSON object");
1890        assert_eq!(states.len(), 1);
1891        assert_eq!(
1892            states["6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9"]["subscribers"],
1893            3
1894        );
1895
1896        // Bincode round-trip must also still work for the same value
1897        // (the new wire format is the contract; older clients are
1898        // documented as incompatible in CHANGELOG).
1899        let bytes = bincode::serialize(&response).expect("bincode must serialize");
1900        let decoded: NodeDiagnosticsResponse =
1901            bincode::deserialize(&bytes).expect("bincode must round-trip");
1902        assert_eq!(
1903            decoded.contract_states.len(),
1904            1,
1905            "bincode round-trip preserves contract_states entries"
1906        );
1907    }
1908}
1909
1910#[cfg(test)]
1911mod client_request_test {
1912    use crate::client_api::{ContractRequest, TryFromFbs};
1913    use crate::contract_interface::UpdateData;
1914    use crate::generated::client_request::root_as_client_request;
1915
1916    const EXPECTED_ENCODED_CONTRACT_ID: &str = "6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9";
1917
1918    #[test]
1919    fn test_build_contract_put_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
1920        let put_req_op = vec![
1921            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,
1922            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,
1923            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,
1924            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,
1925            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,
1926            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,
1927            85, 111, 11, 171, 40, 85, 240, 177, 207, 81, 106, 157, 173, 90, 234, 2, 250, 253, 75,
1928            210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17, 108, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6,
1929            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,
1930            3, 4, 5, 6, 7, 8, 8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8,
1931        ];
1932        let request = if let Ok(client_request) = root_as_client_request(&put_req_op) {
1933            let contract_request = client_request.client_request_as_contract_request().unwrap();
1934            ContractRequest::try_decode_fbs(&contract_request)?
1935        } else {
1936            panic!("failed to decode client request")
1937        };
1938
1939        match request {
1940            ContractRequest::Put {
1941                contract,
1942                state,
1943                related_contracts: _,
1944                subscribe,
1945                blocking_subscribe,
1946            } => {
1947                assert_eq!(
1948                    contract.to_string(),
1949                    "WasmContainer([api=0.0.1](D8fdVLbRyMLw5mZtPRpWMFcrXGN2z8Nq8UGcLGPFBg2W))"
1950                );
1951                assert_eq!(contract.unwrap_v1().data.data(), &[1, 2, 3, 4, 5, 6, 7, 8]);
1952                assert_eq!(state.to_vec(), &[1, 2, 3, 4, 5, 6, 7, 8]);
1953                assert!(!subscribe);
1954                assert!(!blocking_subscribe);
1955            }
1956            _ => panic!("wrong contract request type"),
1957        }
1958
1959        Ok(())
1960    }
1961
1962    #[test]
1963    fn test_build_contract_get_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
1964        let get_req_op = vec![
1965            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,
1966            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,
1967            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,
1968            4, 0, 0, 0, 32, 0, 0, 0, 85, 111, 11, 171, 40, 85, 240, 177, 207, 81, 106, 157, 173,
1969            90, 234, 2, 250, 253, 75, 210, 62, 7, 6, 34, 75, 26, 229, 230, 107, 167, 17, 108,
1970        ];
1971        let request = if let Ok(client_request) = root_as_client_request(&get_req_op) {
1972            let contract_request = client_request.client_request_as_contract_request().unwrap();
1973            ContractRequest::try_decode_fbs(&contract_request)?
1974        } else {
1975            panic!("failed to decode client request")
1976        };
1977
1978        match request {
1979            ContractRequest::Get {
1980                key,
1981                return_contract_code: fetch_contract,
1982                subscribe,
1983                blocking_subscribe,
1984            } => {
1985                assert_eq!(key.encode(), EXPECTED_ENCODED_CONTRACT_ID);
1986                assert!(!fetch_contract);
1987                assert!(!subscribe);
1988                assert!(!blocking_subscribe);
1989            }
1990            _ => panic!("wrong contract request type"),
1991        }
1992
1993        Ok(())
1994    }
1995
1996    #[test]
1997    fn test_build_contract_update_op_from_fbs() -> Result<(), Box<dyn std::error::Error>> {
1998        let update_op = vec![
1999            4, 0, 0, 0, 220, 255, 255, 255, 8, 0, 0, 0, 0, 0, 0, 1, 232, 255, 255, 255, 8, 0, 0, 0,
2000            0, 0, 0, 2, 204, 255, 255, 255, 16, 0, 0, 0, 52, 0, 0, 0, 8, 0, 12, 0, 11, 0, 4, 0, 8,
2001            0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 2, 210, 255, 255, 255, 4, 0, 0, 0, 8, 0, 0, 0, 1, 2, 3,
2002            4, 5, 6, 7, 8, 8, 0, 12, 0, 8, 0, 4, 0, 8, 0, 0, 0, 8, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0,
2003            0, 0, 0, 6, 0, 8, 0, 4, 0, 6, 0, 0, 0, 4, 0, 0, 0, 32, 0, 0, 0, 85, 111, 11, 171, 40,
2004            85, 240, 177, 207, 81, 106, 157, 173, 90, 234, 2, 250, 253, 75, 210, 62, 7, 6, 34, 75,
2005            26, 229, 230, 107, 167, 17, 108,
2006        ];
2007        let request = if let Ok(client_request) = root_as_client_request(&update_op) {
2008            let contract_request = client_request.client_request_as_contract_request().unwrap();
2009            ContractRequest::try_decode_fbs(&contract_request)?
2010        } else {
2011            panic!("failed to decode client request")
2012        };
2013
2014        match request {
2015            ContractRequest::Update { key, data } => {
2016                assert_eq!(
2017                    key.encoded_contract_id(),
2018                    "6kVs66bKaQAC6ohr8b43SvJ95r36tc2hnG7HezmaJHF9"
2019                );
2020                match data {
2021                    UpdateData::Delta(delta) => {
2022                        assert_eq!(delta.to_vec(), &[1, 2, 3, 4, 5, 6, 7, 8])
2023                    }
2024                    _ => panic!("wrong update data type"),
2025                }
2026            }
2027            _ => panic!("wrong contract request type"),
2028        }
2029
2030        Ok(())
2031    }
2032
2033    /// The flatbuffers decode path must NOT panic on an unknown
2034    /// `ContractRequestType` discriminant. Same class as the `DelegateRequest`
2035    /// guard: the generated union verifier accepts any discriminant it doesn't
2036    /// recognize (`_ => Ok(())`), and the union type field is a raw `u8` a
2037    /// client can set to any value. Before the fix this hit `unreachable!()`
2038    /// and downed the connection handler; now it is a clean per-request error.
2039    #[test]
2040    fn fbs_decode_rejects_unknown_contract_discriminant() {
2041        use crate::generated::client_request::{
2042            finish_client_request_buffer, ClientRequest as FbsClientRequest, ClientRequestArgs,
2043            ClientRequestType, ContractRequest as FbsContractRequest, ContractRequestArgs,
2044            ContractRequestType, DelegateKey as FbsDelegateKey, DelegateKeyArgs,
2045            UnregisterDelegate, UnregisterDelegateArgs,
2046        };
2047
2048        let mut b = flatbuffers::FlatBufferBuilder::new();
2049        // Any well-formed table satisfies the required union value; the
2050        // discriminant is unknown so the verifier never inspects it.
2051        let key = b.create_vector(&[0u8; 32]);
2052        let code_hash = b.create_vector(&[0u8; 32]);
2053        let dk = FbsDelegateKey::create(
2054            &mut b,
2055            &DelegateKeyArgs {
2056                key: Some(key),
2057                code_hash: Some(code_hash),
2058            },
2059        );
2060        let dummy = UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
2061        // 99 is past the max known ContractRequestType (Subscribe = 4).
2062        let contract = FbsContractRequest::create(
2063            &mut b,
2064            &ContractRequestArgs {
2065                contract_request_type: ContractRequestType(99),
2066                contract_request: Some(dummy.as_union_value()),
2067            },
2068        );
2069        let client = FbsClientRequest::create(
2070            &mut b,
2071            &ClientRequestArgs {
2072                client_request_type: ClientRequestType::ContractRequest,
2073                client_request: Some(contract.as_union_value()),
2074            },
2075        );
2076        finish_client_request_buffer(&mut b, client);
2077        let bytes = b.finished_data().to_vec();
2078
2079        let client =
2080            root_as_client_request(&bytes).expect("verifier accepts an unknown union discriminant");
2081        let fbs_contract = client
2082            .client_request_as_contract_request()
2083            .expect("client_request is a ContractRequest");
2084        assert!(
2085            ContractRequest::try_decode_fbs(&fbs_contract).is_err(),
2086            "an unknown ContractRequestType discriminant must be a clean \
2087             per-request error, never a panic that downs the connection handler"
2088        );
2089    }
2090}
2091
2092/// Wire-format pins for [`DelegateRequest`].
2093///
2094/// `DelegateRequest` crosses the client<->node boundary as bincode (the
2095/// `EncodingProtocol::Native` path in freenet-core; the Rust clients in this
2096/// crate — `browser.rs` and `regular.rs` — both `bincode::serialize` their
2097/// requests). Bincode encodes an enum's variant as a 4-byte little-endian
2098/// `u32` discriminant, so the *order* of the variants is the wire contract:
2099/// reordering or inserting a variant anywhere but the end silently reassigns
2100/// every following tag and breaks already-deployed clients (the v0.2.11
2101/// break class).
2102///
2103/// `RegisterDelegateWithPredecessors` was appended as the LAST variant
2104/// precisely so the three pre-existing tags stay byte-identical. These tests
2105/// exist because, before this module, `DelegateRequest` had NO wire-format
2106/// pin at all — a reorder would have shipped undetected.
2107#[cfg(test)]
2108mod delegate_request_wire_format {
2109    use super::DelegateRequest;
2110    use crate::code_hash::CodeHash;
2111    use crate::prelude::{
2112        ApplicationMessage, Delegate, DelegateCode, DelegateContainer, DelegateKey,
2113        DelegateWasmAPIVersion, InboundDelegateMsg, Parameters,
2114    };
2115
2116    fn sample_container() -> DelegateContainer {
2117        let code = DelegateCode::from(vec![1u8, 2, 3, 4]);
2118        let params = Parameters::from(vec![9u8, 8, 7]);
2119        DelegateContainer::Wasm(DelegateWasmAPIVersion::V1(Delegate::from((&code, &params))))
2120    }
2121
2122    fn sample_key(fill: u8) -> DelegateKey {
2123        DelegateKey::new([fill; 32], CodeHash::new([fill.wrapping_add(1); 32]))
2124    }
2125
2126    // The four sample values whose complete bincode encodings are frozen in
2127    // `wire_format_is_frozen`. Kept byte-for-byte identical to the throwaway
2128    // generator that produced the frozen vectors, so the freeze is
2129    // reproducible: construct the value, `bincode::serialize`, compare.
2130    fn sample_app_messages() -> DelegateRequest<'static> {
2131        DelegateRequest::ApplicationMessages {
2132            key: DelegateKey::new([0x11; 32], CodeHash::new([0x22; 32])),
2133            params: Parameters::from(vec![0xDE, 0xAD, 0xBE, 0xEF]),
2134            inbound: vec![InboundDelegateMsg::ApplicationMessage(
2135                ApplicationMessage::new(vec![0x01, 0x02, 0x03]),
2136            )],
2137        }
2138    }
2139
2140    fn sample_register() -> DelegateRequest<'static> {
2141        DelegateRequest::RegisterDelegate {
2142            delegate: sample_container(),
2143            cipher: [0x55; 32],
2144            nonce: [0x66; 24],
2145        }
2146    }
2147
2148    fn sample_unregister() -> DelegateRequest<'static> {
2149        DelegateRequest::UnregisterDelegate(DelegateKey::new([0x11; 32], CodeHash::new([0x22; 32])))
2150    }
2151
2152    fn sample_register_with_predecessors() -> DelegateRequest<'static> {
2153        DelegateRequest::RegisterDelegateWithPredecessors {
2154            delegate: sample_container(),
2155            cipher: [0x33; 32],
2156            nonce: [0x44; 24],
2157            predecessors: vec![sample_key(0xA0), sample_key(0xB0), sample_key(0xC0)],
2158        }
2159    }
2160
2161    /// Complete-byte wire-format freeze for all four variants.
2162    ///
2163    /// The three pre-existing variants (`ApplicationMessages`,
2164    /// `RegisterDelegate`, `UnregisterDelegate`) are pinned to their FULL
2165    /// expected byte vectors — not just the 4-byte tag — so a field reorder or
2166    /// a change to a nested type's encoding (e.g. `DelegateContainer`,
2167    /// `DelegateKey`, `ApplicationMessage`) is caught, not only a variant
2168    /// reorder. Those three vectors were generated from **origin/main @
2169    /// 8b53702** (the shipped format) via a throwaway generator and confirmed
2170    /// byte-identical to this branch's output, so the pin anchors to what old
2171    /// clients actually speak. The fourth vector
2172    /// (`RegisterDelegateWithPredecessors`, multi-predecessor list) was
2173    /// generated from THIS branch.
2174    ///
2175    /// The test also DESERIALIZES each frozen vector, proving an old-format
2176    /// byte stream still decodes into the expected variant on this build.
2177    #[test]
2178    fn wire_format_is_frozen() {
2179        // --- origin/main @ 8b53702 (shipped format) ---
2180        const APP_MESSAGES: &[u8] = &[
2181            0, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
2182            17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2183            34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2184            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,
2185            0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0,
2186        ];
2187        const REGISTER: &[u8] = &[
2188            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,
2189            0, 0, 1, 2, 3, 4, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5, 141, 135, 18, 213, 208,
2190            81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160, 84, 50, 88, 111, 44, 39,
2191            24, 219, 97, 92, 222, 20, 205, 248, 149, 154, 214, 38, 193, 144, 31, 141, 32, 222, 49,
2192            197, 66, 237, 16, 98, 165, 72, 6, 11, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5,
2193            141, 135, 18, 213, 208, 81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160,
2194            84, 50, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85,
2195            85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 85, 102, 102, 102, 102, 102, 102, 102, 102,
2196            102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102,
2197        ];
2198        const UNREGISTER: &[u8] = &[
2199            2, 0, 0, 0, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17,
2200            17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2201            34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34,
2202            34,
2203        ];
2204        // --- this branch (the new variant) ---
2205        const REGISTER_WITH_PREDECESSORS: &[u8] = &[
2206            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,
2207            0, 0, 1, 2, 3, 4, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5, 141, 135, 18, 213, 208,
2208            81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160, 84, 50, 88, 111, 44, 39,
2209            24, 219, 97, 92, 222, 20, 205, 248, 149, 154, 214, 38, 193, 144, 31, 141, 32, 222, 49,
2210            197, 66, 237, 16, 98, 165, 72, 6, 11, 99, 120, 29, 23, 20, 37, 163, 99, 18, 250, 5,
2211            141, 135, 18, 213, 208, 81, 53, 169, 145, 236, 32, 53, 28, 233, 214, 92, 219, 25, 160,
2212            84, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51,
2213            51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68,
2214            68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 3, 0, 0, 0, 0, 0, 0, 0, 160,
2215            160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160,
2216            160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 160, 161, 161, 161,
2217            161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161,
2218            161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 161, 176, 176, 176, 176, 176,
2219            176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 176,
2220            176, 176, 176, 176, 176, 176, 176, 176, 176, 176, 177, 177, 177, 177, 177, 177, 177,
2221            177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177, 177,
2222            177, 177, 177, 177, 177, 177, 177, 177, 192, 192, 192, 192, 192, 192, 192, 192, 192,
2223            192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
2224            192, 192, 192, 192, 192, 192, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193,
2225            193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193, 193,
2226            193, 193, 193, 193,
2227        ];
2228
2229        // 1. Serialization is byte-stable for every variant.
2230        assert_eq!(
2231            bincode::serialize(&sample_app_messages()).unwrap(),
2232            APP_MESSAGES,
2233            "ApplicationMessages (tag 0) encoding changed"
2234        );
2235        assert_eq!(
2236            bincode::serialize(&sample_register()).unwrap(),
2237            REGISTER,
2238            "RegisterDelegate (tag 1) encoding changed"
2239        );
2240        assert_eq!(
2241            bincode::serialize(&sample_unregister()).unwrap(),
2242            UNREGISTER,
2243            "UnregisterDelegate (tag 2) encoding changed"
2244        );
2245        assert_eq!(
2246            bincode::serialize(&sample_register_with_predecessors()).unwrap(),
2247            REGISTER_WITH_PREDECESSORS,
2248            "RegisterDelegateWithPredecessors (tag 3) encoding changed"
2249        );
2250
2251        // 2. The four variant tags are exactly 0,1,2,3 — appending the new
2252        //    variant did not shift the pre-existing three.
2253        assert_eq!(APP_MESSAGES[..4], 0u32.to_le_bytes());
2254        assert_eq!(REGISTER[..4], 1u32.to_le_bytes());
2255        assert_eq!(UNREGISTER[..4], 2u32.to_le_bytes());
2256        assert_eq!(REGISTER_WITH_PREDECESSORS[..4], 3u32.to_le_bytes());
2257
2258        // 3. Each frozen (old-format) byte stream still DECODES into its
2259        //    variant on this build — an old client's bytes remain readable.
2260        assert!(matches!(
2261            bincode::deserialize::<DelegateRequest>(APP_MESSAGES).unwrap(),
2262            DelegateRequest::ApplicationMessages { .. }
2263        ));
2264        assert!(matches!(
2265            bincode::deserialize::<DelegateRequest>(REGISTER).unwrap(),
2266            DelegateRequest::RegisterDelegate { .. }
2267        ));
2268        assert!(matches!(
2269            bincode::deserialize::<DelegateRequest>(UNREGISTER).unwrap(),
2270            DelegateRequest::UnregisterDelegate(_)
2271        ));
2272        assert!(matches!(
2273            bincode::deserialize::<DelegateRequest>(REGISTER_WITH_PREDECESSORS).unwrap(),
2274            DelegateRequest::RegisterDelegateWithPredecessors { .. }
2275        ));
2276    }
2277
2278    /// The new variant's tag is exactly `3u32` little-endian — a direct,
2279    /// self-documenting assertion that it was appended one past
2280    /// `UnregisterDelegate` (tag 2).
2281    #[test]
2282    fn register_with_predecessors_tag_is_three() {
2283        let req = DelegateRequest::RegisterDelegateWithPredecessors {
2284            delegate: sample_container(),
2285            cipher: [0; 32],
2286            nonce: [0; 24],
2287            predecessors: vec![],
2288        };
2289        assert_eq!(
2290            bincode::serialize(&req).unwrap()[..4],
2291            3u32.to_le_bytes(),
2292            "RegisterDelegateWithPredecessors must be the 4th variant (tag 3)"
2293        );
2294    }
2295
2296    /// The new variant round-trips through bincode, including a multi-element
2297    /// `predecessors` list (the skip-generations case the variant exists for).
2298    #[test]
2299    fn register_with_predecessors_round_trips() {
2300        let predecessors = vec![sample_key(0xA0), sample_key(0xB0), sample_key(0xC0)];
2301        let delegate = sample_container();
2302        let expected_key = delegate.key().clone();
2303
2304        let req = DelegateRequest::RegisterDelegateWithPredecessors {
2305            delegate,
2306            cipher: [0x33; 32],
2307            nonce: [0x44; 24],
2308            predecessors: predecessors.clone(),
2309        };
2310
2311        let bytes = bincode::serialize(&req).unwrap();
2312        let decoded: DelegateRequest = bincode::deserialize(&bytes).unwrap();
2313
2314        match decoded {
2315            DelegateRequest::RegisterDelegateWithPredecessors {
2316                delegate,
2317                cipher,
2318                nonce,
2319                predecessors: got,
2320            } => {
2321                assert_eq!(delegate.key(), &expected_key, "delegate preserved");
2322                assert_eq!(cipher, [0x33; 32], "cipher preserved");
2323                assert_eq!(nonce, [0x44; 24], "nonce preserved");
2324                assert_eq!(
2325                    got, predecessors,
2326                    "full predecessor list preserved in order"
2327                );
2328            }
2329            other => panic!("round-trip produced the wrong variant: {other:?}"),
2330        }
2331    }
2332
2333    /// `key()` returns the newly-registered delegate's key (not a
2334    /// predecessor's), matching `RegisterDelegate`'s behavior.
2335    #[test]
2336    fn key_returns_the_new_delegate() {
2337        let delegate = sample_container();
2338        let expected_key = delegate.key().clone();
2339        let req = DelegateRequest::RegisterDelegateWithPredecessors {
2340            delegate,
2341            cipher: [0; 32],
2342            nonce: [0; 24],
2343            predecessors: vec![sample_key(0xA0)],
2344        };
2345        assert_eq!(req.key(), &expected_key);
2346    }
2347
2348    /// The flatbuffers decode path must NOT panic on an unknown
2349    /// `DelegateRequestType` discriminant. The generated union verifier accepts
2350    /// any discriminant it doesn't recognize (`_ => Ok(())`), and the union
2351    /// type field is a raw `u8` that the public (TypeScript) builder can set to
2352    /// any value, so a crafted request reaches `DelegateRequest::try_decode_fbs`
2353    /// with an out-of-range discriminant. Before the fix this hit
2354    /// `unreachable!()` and took down the connection handler; now it is a clean
2355    /// per-request error. (Regression guard for the P2 finding on PR #86.)
2356    #[test]
2357    fn fbs_decode_rejects_unknown_discriminant() {
2358        use crate::client_api::TryFromFbs;
2359        use crate::generated::client_request::{
2360            finish_client_request_buffer, root_as_client_request,
2361            ClientRequest as FbsClientRequest, ClientRequestArgs, ClientRequestType,
2362            DelegateKey as FbsDelegateKey, DelegateKeyArgs, DelegateRequest as FbsDelegateRequest,
2363            DelegateRequestArgs, DelegateRequestType, UnregisterDelegate, UnregisterDelegateArgs,
2364        };
2365
2366        let mut b = flatbuffers::FlatBufferBuilder::new();
2367        // A real, well-formed UnregisterDelegate table to hang off the union...
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 unreg = UnregisterDelegate::create(&mut b, &UnregisterDelegateArgs { key: Some(dk) });
2378        // ...but LIE about the union type: 99 is past the max known
2379        // discriminant (UnregisterDelegate = 3), yet the verifier accepts it.
2380        let dreq = FbsDelegateRequest::create(
2381            &mut b,
2382            &DelegateRequestArgs {
2383                delegate_request_type: DelegateRequestType(99),
2384                delegate_request: Some(unreg.as_union_value()),
2385            },
2386        );
2387        let creq = FbsClientRequest::create(
2388            &mut b,
2389            &ClientRequestArgs {
2390                client_request_type: ClientRequestType::DelegateRequest,
2391                client_request: Some(dreq.as_union_value()),
2392            },
2393        );
2394        finish_client_request_buffer(&mut b, creq);
2395        let bytes = b.finished_data().to_vec();
2396
2397        let client =
2398            root_as_client_request(&bytes).expect("verifier accepts an unknown union discriminant");
2399        let fbs_delegate = client
2400            .client_request_as_delegate_request()
2401            .expect("client_request is a DelegateRequest");
2402        let decoded = DelegateRequest::try_decode_fbs(&fbs_delegate);
2403        assert!(
2404            decoded.is_err(),
2405            "an unknown DelegateRequestType discriminant must be a clean \
2406             per-request error, never a panic that downs the connection handler"
2407        );
2408    }
2409}