Skip to main content

freenet_stdlib/client_api/
client_events.rs

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