Skip to main content

linera_sdk/
formats.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Support the declaration of the binary formats used by an application.
5
6/// Re-exports the `#[derive(StableEnum)]` macro for stable-tagged enums.
7pub use linera_sdk_derive::StableEnum;
8use serde::{de::DeserializeOwned, Deserialize, Serialize};
9
10/// Private re-exports of crates referenced by the macros in `linera-sdk-derive`.
11/// Lets downstream crates use the macros without taking direct dependencies on
12/// the crates being re-exported. Not part of the public API; do not use
13/// directly.
14#[doc(hidden)]
15pub mod __private {
16    pub use serde;
17    pub use serde_reflection;
18}
19#[cfg(not(target_arch = "wasm32"))]
20use serde_reflection::TracerConfig;
21use serde_reflection::{
22    json_converter::{
23        DeserializationContext, DeserializationEnvironment, SerializationContext,
24        SerializationEnvironment, SymbolTableEnvironment,
25    },
26    Format, Registry, Samples, Tracer,
27};
28
29/// The serde formats used by an application. The exact serde encoding in use must be
30/// known separately.
31#[derive(Serialize, Deserialize, Debug, Eq, Clone, PartialEq)]
32#[serde(rename_all = "UPPERCASE")]
33pub struct Formats {
34    /// The registry of container definitions.
35    pub registry: Registry,
36    /// The format of operations.
37    pub operation: Format,
38    /// The format of operation responses.
39    pub response: Format,
40    /// The format of messages.
41    pub message: Format,
42    /// The format of events.
43    pub event_value: Format,
44}
45
46/// An application using BCS as binary encoding.
47pub trait BcsApplication {
48    /// Link the public Abi of application for good measure.
49    type Abi;
50
51    /// Returns the serde formats for this application's ABI types. The returned
52    /// registry is self-contained: it describes every type structurally, including
53    /// the well-known linera-base primitives.
54    fn formats() -> serde_reflection::Result<Formats>;
55
56    /// Like [`formats`](Self::formats), but with the well-known linera-base primitives
57    /// pruned from the registry (see [`Formats::prune_known_primitives`]) so that they
58    /// decode to their human-readable form via [`LineraEnvironment`]. This is the form
59    /// meant to be published to a formats registry.
60    #[cfg(not(target_arch = "wasm32"))]
61    fn pruned_formats() -> Result<Formats, PruneError> {
62        let mut formats = Self::formats()?;
63        formats.prune_known_primitives()?;
64        Ok(formats)
65    }
66}
67
68/// Companion trait of [`StableEnum`]: exposes each variant's stable tag and
69/// provides the implementation backing
70/// [`TracerExt::trace_stable_enum_type`]. Implemented by the
71/// `#[derive(StableEnum)]` macro.
72///
73/// The derive auto-generates `trace_all_variants` by calling
74/// [`Tracer::trace_type_once`] for each field type to obtain a sample value,
75/// then [`Tracer::trace_value`] to record the variant. As a result, every
76/// field type must implement [`serde::de::DeserializeOwned`] (or otherwise be
77/// traceable by [`Tracer::trace_type_once`]). Nested `StableEnum` fields are
78/// not supported automatically — pre-trace them with
79/// `trace_stable_enum_type::<NestedEnum>` first.
80pub trait StableEnumTrace: Sized + Serialize {
81    /// The `(variant_name, variant_tag)` pairs in declaration order.
82    const STABLE_VARIANTS: &'static [(&'static str, u32)];
83
84    /// Trace each variant of `Self` into `tracer`'s registry. The default
85    /// derive implementation is sufficient for most cases.
86    fn trace_all_variants(
87        tracer: &mut Tracer,
88        samples: &Samples,
89    ) -> serde_reflection::Result<Format>;
90}
91
92/// Marker trait for enums whose variant tags on the wire are derived from
93/// `Keccak-256(variant_name)`. Apply with `#[derive(StableEnum)]`.
94///
95/// The blanket impl below covers every type for which all three of
96/// [`Serialize`], [`DeserializeOwned`], and [`StableEnumTrace`] are
97/// implemented — `#[derive(StableEnum)]` emits all three at once.
98pub trait StableEnum: StableEnumTrace + Serialize + DeserializeOwned {}
99
100impl<T> StableEnum for T where T: StableEnumTrace + Serialize + DeserializeOwned {}
101
102/// Extension methods on [`Tracer`] for tracing enums whose variant tags are
103/// not contiguous starting at zero.
104///
105/// The standard [`Tracer::trace_type`] discovers an enum's variants by probing
106/// `0, 1, 2, …` until each `u32` index has been seen. With Keccak-derived
107/// stable tags those indices are not consecutive (they live in `[2^27, 2^28)`),
108/// so probing never terminates. This trait delegates to the enum's
109/// [`StableEnumTrace`] impl, which drives tracing variant-by-variant via the
110/// enum's [`Serialize`] impl.
111pub trait TracerExt {
112    /// Trace every variant of a stable-tagged enum, returning the enum's
113    /// [`Format`] for use in [`Formats::operation`], [`Formats::response`],
114    /// etc.
115    fn trace_stable_enum_type<T>(&mut self, samples: &Samples) -> serde_reflection::Result<Format>
116    where
117        T: StableEnumTrace;
118}
119
120impl TracerExt for Tracer {
121    fn trace_stable_enum_type<T>(&mut self, samples: &Samples) -> serde_reflection::Result<Format>
122    where
123        T: StableEnumTrace,
124    {
125        T::trace_all_variants(self, samples)
126    }
127}
128
129/// Decode BCS-serialized `bytes` into a [`serde_json::Value`], guided by `format`
130/// and the container `registry`.
131///
132/// Well-known linera-base primitives that are absent from `registry` (because they
133/// were removed by [`Formats::prune_known_primitives`]) are decoded into their
134/// human-readable representation by [`LineraEnvironment`].
135fn bcs_to_json(
136    bytes: &[u8],
137    format: &Format,
138    registry: &Registry,
139) -> bcs::Result<serde_json::Value> {
140    let context = DeserializationContext {
141        format: format.clone(),
142        registry,
143        environment: &LineraEnvironment,
144    };
145    bcs::from_bytes_seed(context, bytes)
146}
147
148/// Encode a [`serde_json::Value`] into BCS `bytes`, guided by `format` and the
149/// container `registry`. This is the inverse of [`bcs_to_json`].
150///
151/// Well-known linera-base primitives that are absent from `registry` are read from
152/// their human-readable representation by [`LineraEnvironment`].
153fn json_to_bcs(
154    value: &serde_json::Value,
155    format: &Format,
156    registry: &Registry,
157) -> bcs::Result<Vec<u8>> {
158    let context = SerializationContext {
159        value,
160        format,
161        registry,
162        environment: &LineraEnvironment,
163    };
164    bcs::to_bytes(&context)
165}
166
167/// Decodes the BCS form of a value as the concrete type `T`, then re-encodes it as
168/// JSON using `T`'s human-readable serialization.
169fn primitive_to_json<'de, T, D>(deserializer: D) -> Result<serde_json::Value, String>
170where
171    T: serde::Deserialize<'de> + serde::Serialize,
172    D: serde::Deserializer<'de>,
173{
174    let value = T::deserialize(deserializer).map_err(|error| error.to_string())?;
175    serde_json::to_value(&value).map_err(|error| error.to_string())
176}
177
178/// Reads a value's human-readable JSON representation into the concrete type `T`, then
179/// serializes it with `serializer` (its BCS form). Inverse of [`primitive_to_json`].
180fn primitive_from_json<T, S>(value: &serde_json::Value, serializer: S) -> Result<S::Ok, S::Error>
181where
182    T: serde::Serialize + serde::de::DeserializeOwned,
183    S: serde::Serializer,
184{
185    let value: T = T::deserialize(value).map_err(serde::ser::Error::custom)?;
186    value.serialize(serializer)
187}
188
189/// Declares the linera-base primitives whose human-readable serde representation
190/// differs from their BCS one. This single list drives both [`LineraEnvironment`]
191/// (decoding) and the canonical-format check used by
192/// [`Formats::prune_known_primitives`], so the two can never drift apart.
193macro_rules! known_human_readable_primitives {
194    ($($name:literal => $ty:ty),* $(,)?) => {
195        /// The registry names of the linera-base primitives handled by
196        /// [`LineraEnvironment`]. These are exactly the types whose human-readable
197        /// serde representation differs from their BCS form *and* whose BCS form is a
198        /// named container (so it can be matched by name in a traced registry).
199        pub const KNOWN_PRIMITIVE_NAMES: &[&str] = &[$($name),*];
200
201        /// A [`json_converter`](serde_reflection::json_converter) environment that
202        /// decodes the BCS form of well-known linera-base primitives into their
203        /// human-readable JSON representation (e.g. a `CryptoHash` as a hex string, an
204        /// `Amount` as a decimal string, an `AccountOwner` as its canonical address).
205        ///
206        /// It only takes effect for names that are *absent* from the registry being
207        /// decoded; see [`Formats::prune_known_primitives`].
208        #[derive(Clone, Copy, Debug, Default)]
209        pub struct LineraEnvironment;
210
211        impl SymbolTableEnvironment for LineraEnvironment {}
212
213        impl<'de> DeserializationEnvironment<'de> for LineraEnvironment {
214            fn deserialize<D>(
215                &self,
216                name: String,
217                deserializer: D,
218            ) -> Result<serde_json::Value, String>
219            where
220                D: serde::Deserializer<'de>,
221            {
222                match name.as_str() {
223                    $( $name => primitive_to_json::<$ty, D>(deserializer), )*
224                    _ => Err(format!("No external definition available for {name}")),
225                }
226            }
227        }
228
229        impl SerializationEnvironment for LineraEnvironment {
230            fn serialize<S>(
231                &self,
232                name: &str,
233                value: &serde_json::Value,
234                serializer: S,
235            ) -> Result<S::Ok, S::Error>
236            where
237                S: serde::Serializer,
238            {
239                match name {
240                    $( $name => primitive_from_json::<$ty, S>(value, serializer), )*
241                    _ => Err(serde::ser::Error::custom(format!(
242                        "No external serializer available for {name}"
243                    ))),
244                }
245            }
246        }
247
248        /// Traces the canonical BCS format of every known primitive, used to verify
249        /// the correspondence before pruning.
250        #[cfg(not(target_arch = "wasm32"))]
251        fn expected_primitive_registry() -> serde_reflection::Result<Registry> {
252            let mut tracer = Tracer::new(
253                TracerConfig::default()
254                    .record_samples_for_newtype_structs(true)
255                    .record_samples_for_tuple_structs(true),
256            );
257            let samples = Samples::new();
258            $( tracer.trace_type::<$ty>(&samples)?; )*
259            // Supporting enums reached only through the primitives above; they must be
260            // traced explicitly so all of their variants are recorded.
261            tracer.trace_type::<crate::linera_base_types::VmRuntime>(&samples)?;
262            tracer.trace_type::<crate::linera_base_types::BlobType>(&samples)?;
263            tracer.trace_type::<crate::linera_base_types::GenericApplicationId>(&samples)?;
264            tracer.registry()
265        }
266    };
267}
268
269// NOTE: The cryptographic key and signature types (`Ed25519PublicKey`,
270// `Secp256k1PublicKey`, `EvmPublicKey`, and their `*Signature` counterparts) also have
271// a customized human-readable serde representation, but their `Deserialize` validates
272// the bytes (e.g. that a compressed point is on the curve), so `serde_reflection`
273// cannot trace them from dummy bytes and we cannot verify their format before pruning.
274// Supporting them would require feeding valid samples to the tracer; deferred for now.
275// They also do not currently appear in any application's operation/message ABI.
276known_human_readable_primitives! {
277    "CryptoHash" => crate::linera_base_types::CryptoHash,
278    "AccountOwner" => crate::linera_base_types::AccountOwner,
279    "Amount" => crate::linera_base_types::Amount,
280    "Epoch" => crate::linera_base_types::Epoch,
281    "BlobId" => crate::linera_base_types::BlobId,
282    "StreamId" => crate::linera_base_types::StreamId,
283    "ModuleId" => crate::linera_base_types::ModuleId,
284    "ApplicationId" => crate::linera_base_types::ApplicationId,
285}
286
287/// An error raised while pruning known primitives from a [`Formats`] registry.
288#[cfg(not(target_arch = "wasm32"))]
289#[derive(Debug, thiserror::Error)]
290pub enum PruneError {
291    /// The canonical formats of the known primitives could not be computed.
292    #[error("failed to compute the canonical primitive formats: {0}")]
293    Reflection(#[from] serde_reflection::Error),
294    /// A known primitive name is present in the registry but with a format that does
295    /// not match the canonical linera-base one (e.g. a name collision or a layout
296    /// change). Nothing is pruned in that case.
297    #[error(
298        "registry entry for `{name}` does not match the canonical linera-base format; \
299         refusing to prune"
300    )]
301    Mismatch {
302        /// The name of the offending registry entry.
303        name: String,
304    },
305}
306
307impl Formats {
308    /// Decode BCS-encoded operation bytes into a JSON value.
309    pub fn decode_operation(&self, bytes: &[u8]) -> bcs::Result<serde_json::Value> {
310        bcs_to_json(bytes, &self.operation, &self.registry)
311    }
312
313    /// Decode BCS-encoded operation response bytes into a JSON value.
314    pub fn decode_response(&self, bytes: &[u8]) -> bcs::Result<serde_json::Value> {
315        bcs_to_json(bytes, &self.response, &self.registry)
316    }
317
318    /// Decode BCS-encoded message bytes into a JSON value.
319    pub fn decode_message(&self, bytes: &[u8]) -> bcs::Result<serde_json::Value> {
320        bcs_to_json(bytes, &self.message, &self.registry)
321    }
322
323    /// Decode BCS-encoded event value bytes into a JSON value.
324    pub fn decode_event_value(&self, bytes: &[u8]) -> bcs::Result<serde_json::Value> {
325        bcs_to_json(bytes, &self.event_value, &self.registry)
326    }
327
328    /// Encode a JSON operation value into its BCS bytes. Inverse of
329    /// [`decode_operation`](Self::decode_operation).
330    pub fn encode_operation(&self, value: &serde_json::Value) -> bcs::Result<Vec<u8>> {
331        json_to_bcs(value, &self.operation, &self.registry)
332    }
333
334    /// Encode a JSON operation response value into its BCS bytes. Inverse of
335    /// [`decode_response`](Self::decode_response).
336    pub fn encode_response(&self, value: &serde_json::Value) -> bcs::Result<Vec<u8>> {
337        json_to_bcs(value, &self.response, &self.registry)
338    }
339
340    /// Encode a JSON message value into its BCS bytes. Inverse of
341    /// [`decode_message`](Self::decode_message).
342    pub fn encode_message(&self, value: &serde_json::Value) -> bcs::Result<Vec<u8>> {
343        json_to_bcs(value, &self.message, &self.registry)
344    }
345
346    /// Encode a JSON event value into its BCS bytes. Inverse of
347    /// [`decode_event_value`](Self::decode_event_value).
348    pub fn encode_event_value(&self, value: &serde_json::Value) -> bcs::Result<Vec<u8>> {
349        json_to_bcs(value, &self.event_value, &self.registry)
350    }
351
352    /// Removes the registry entries for the well-known linera-base primitives (see
353    /// [`KNOWN_PRIMITIVE_NAMES`]) so that decoding falls back to [`LineraEnvironment`],
354    /// which renders them in their human-readable form.
355    ///
356    /// Before removing anything, this verifies that each such entry actually matches
357    /// the canonical BCS format of the corresponding linera-base type. If any entry is
358    /// present with a different format — for instance because an application defined a
359    /// distinct type with a colliding name — this returns [`PruneError::Mismatch`] and
360    /// leaves the registry untouched.
361    ///
362    /// This is meant to be called from snapshot tests (and any other code that
363    /// generates the stored [`Formats`]) so that the human-readable rendering is baked
364    /// into the published formats.
365    #[cfg(not(target_arch = "wasm32"))]
366    pub fn prune_known_primitives(&mut self) -> Result<(), PruneError> {
367        let expected = expected_primitive_registry()?;
368        // Verify everything first so a mismatch never leaves the registry half-pruned.
369        for name in KNOWN_PRIMITIVE_NAMES {
370            let (Some(actual), Some(expected_format)) =
371                (self.registry.get(*name), expected.get(*name))
372            else {
373                continue;
374            };
375            if actual != expected_format {
376                return Err(PruneError::Mismatch {
377                    name: (*name).to_string(),
378                });
379            }
380        }
381        for name in KNOWN_PRIMITIVE_NAMES {
382            self.registry.remove(*name);
383        }
384        Ok(())
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use serde::{Deserialize, Serialize};
391    use serde_json::json;
392    use serde_reflection::{Samples, Tracer, TracerConfig};
393
394    use super::*;
395
396    fn trace_format<T>() -> (Format, Registry)
397    where
398        T: Serialize + for<'de> Deserialize<'de>,
399    {
400        let mut tracer = Tracer::new(
401            TracerConfig::default()
402                .record_samples_for_newtype_structs(true)
403                .record_samples_for_tuple_structs(true),
404        );
405        let samples = Samples::new();
406        let (format, _) = tracer.trace_type::<T>(&samples).unwrap();
407        let registry = tracer.registry().unwrap();
408        (format, registry)
409    }
410
411    #[test]
412    fn primitive_round_trip() {
413        let (format, registry) = trace_format::<u64>();
414        let bytes = bcs::to_bytes(&42u64).unwrap();
415        let value = bcs_to_json(&bytes, &format, &registry).unwrap();
416        assert_eq!(value, json!(42));
417    }
418
419    #[test]
420    fn struct_round_trip() {
421        #[derive(Serialize, Deserialize)]
422        struct Point {
423            x: i32,
424            y: i32,
425        }
426
427        let (format, registry) = trace_format::<Point>();
428        let bytes = bcs::to_bytes(&Point { x: 10, y: -7 }).unwrap();
429        let value = bcs_to_json(&bytes, &format, &registry).unwrap();
430        assert_eq!(value, json!({ "x": 10, "y": -7 }));
431    }
432
433    #[test]
434    fn enum_unit_and_struct_variants() {
435        #[derive(Serialize, Deserialize)]
436        enum Op {
437            Increment,
438            Set { value: u64 },
439            Add(i64, i64),
440        }
441
442        let (format, registry) = trace_format::<Op>();
443
444        let bytes = bcs::to_bytes(&Op::Increment).unwrap();
445        let value = bcs_to_json(&bytes, &format, &registry).unwrap();
446        assert_eq!(value, json!({ "Increment": null }));
447
448        let bytes = bcs::to_bytes(&Op::Set { value: 99 }).unwrap();
449        let value = bcs_to_json(&bytes, &format, &registry).unwrap();
450        assert_eq!(value, json!({ "Set": { "value": 99 } }));
451
452        let bytes = bcs::to_bytes(&Op::Add(2, 3)).unwrap();
453        let value = bcs_to_json(&bytes, &format, &registry).unwrap();
454        assert_eq!(value, json!({ "Add": [2, 3] }));
455    }
456
457    #[test]
458    fn nested_with_option_and_seq() {
459        #[derive(Serialize, Deserialize)]
460        struct Outer {
461            tag: String,
462            items: Vec<u32>,
463            note: Option<String>,
464        }
465
466        let (format, registry) = trace_format::<Outer>();
467        let value = Outer {
468            tag: "hello".to_string(),
469            items: vec![1, 2, 3],
470            note: None,
471        };
472        let bytes = bcs::to_bytes(&value).unwrap();
473        let json_value = bcs_to_json(&bytes, &format, &registry).unwrap();
474        assert_eq!(
475            json_value,
476            json!({ "tag": "hello", "items": [1, 2, 3], "note": null })
477        );
478    }
479
480    #[test]
481    fn formats_decode_helpers() {
482        #[derive(Serialize, Deserialize)]
483        enum Operation {
484            Ping,
485            Echo(String),
486        }
487        #[derive(Serialize, Deserialize)]
488        struct Response {
489            ok: bool,
490        }
491
492        let (operation, op_registry) = trace_format::<Operation>();
493        let (response, resp_registry) = trace_format::<Response>();
494
495        // Combine the two registries so the same `Formats` can decode both types.
496        let mut registry = op_registry;
497        registry.extend(resp_registry);
498
499        let (message, _) = trace_format::<()>();
500        let (event_value, _) = trace_format::<()>();
501
502        let formats = Formats {
503            registry,
504            operation,
505            response,
506            message,
507            event_value,
508        };
509
510        let op_bytes = bcs::to_bytes(&Operation::Echo("hi".to_string())).unwrap();
511        assert_eq!(
512            formats.decode_operation(&op_bytes).unwrap(),
513            json!({ "Echo": "hi" })
514        );
515
516        let resp_bytes = bcs::to_bytes(&Response { ok: true }).unwrap();
517        assert_eq!(
518            formats.decode_response(&resp_bytes).unwrap(),
519            json!({ "ok": true })
520        );
521
522        // Empty BCS for the `()` unit type.
523        let unit_bytes = bcs::to_bytes(&()).unwrap();
524        assert_eq!(formats.decode_message(&unit_bytes).unwrap(), json!(null));
525        assert_eq!(
526            formats.decode_event_value(&unit_bytes).unwrap(),
527            json!(null)
528        );
529
530        // The encode helpers are the inverse of the decode helpers.
531        assert_eq!(
532            formats.encode_operation(&json!({ "Echo": "hi" })).unwrap(),
533            op_bytes
534        );
535        assert_eq!(
536            formats.encode_response(&json!({ "ok": true })).unwrap(),
537            resp_bytes
538        );
539        assert_eq!(formats.encode_message(&json!(null)).unwrap(), unit_bytes);
540        assert_eq!(
541            formats.encode_event_value(&json!(null)).unwrap(),
542            unit_bytes
543        );
544    }
545
546    #[test]
547    fn malformed_bytes_return_error() {
548        let (format, registry) = trace_format::<u64>();
549        // u64 needs 8 bytes; only provide 3.
550        assert!(bcs_to_json(&[1, 2, 3], &format, &registry).is_err());
551    }
552
553    #[test]
554    fn expected_registry_builds() {
555        let registry = expected_primitive_registry().unwrap();
556        for name in KNOWN_PRIMITIVE_NAMES {
557            assert!(registry.contains_key(*name), "missing {name}");
558        }
559    }
560
561    #[test]
562    fn known_primitives_decode_as_human_readable() {
563        use std::str::FromStr as _;
564
565        use crate::linera_base_types::{AccountOwner, Amount, CryptoHash, ModuleId, VmRuntime};
566
567        #[derive(Serialize, Deserialize)]
568        struct Sample {
569            owner: AccountOwner,
570            amount: Amount,
571            hash: CryptoHash,
572            module: Option<ModuleId>,
573        }
574
575        let hash = CryptoHash::from_str(&"ab".repeat(32)).unwrap();
576        let value = Sample {
577            owner: AccountOwner::Address32(hash),
578            amount: Amount::from_tokens(5),
579            hash,
580            module: Some(ModuleId::new(hash, hash, VmRuntime::Wasm)),
581        };
582
583        // Trace `Sample` plus the nested multi-variant enums, so every variant of
584        // `AccountOwner` and `VmRuntime` is recorded (a single `trace_type` pass over a
585        // struct only samples one variant per nested enum).
586        let mut tracer = Tracer::new(
587            TracerConfig::default()
588                .record_samples_for_newtype_structs(true)
589                .record_samples_for_tuple_structs(true),
590        );
591        let samples = Samples::new();
592        let (operation, _) = tracer.trace_type::<Sample>(&samples).unwrap();
593        tracer.trace_type::<AccountOwner>(&samples).unwrap();
594        tracer.trace_type::<VmRuntime>(&samples).unwrap();
595        let registry = tracer.registry().unwrap();
596
597        let unit = Format::Unit;
598        let mut formats = Formats {
599            registry,
600            operation,
601            response: unit.clone(),
602            message: unit.clone(),
603            event_value: unit,
604        };
605
606        // Tracing this value embeds CryptoHash/AccountOwner/Amount/ModuleId structurally.
607        let bytes = bcs::to_bytes(&value).unwrap();
608        assert!(formats.registry.contains_key("CryptoHash"));
609
610        // After pruning, those primitives are decoded by `LineraEnvironment` into the
611        // exact human-readable JSON their own `Serialize` would produce.
612        formats.prune_known_primitives().unwrap();
613        assert!(!formats.registry.contains_key("CryptoHash"));
614        assert!(!formats.registry.contains_key("AccountOwner"));
615
616        let decoded = formats.decode_operation(&bytes).unwrap();
617        let expected = serde_json::to_value(&value).unwrap();
618        assert_eq!(decoded, expected);
619        // Sanity-check the human-readable shape: a hex hash and a decimal amount string.
620        assert_eq!(decoded["hash"], json!("ab".repeat(32)));
621        assert_eq!(decoded["amount"], json!(value.amount.to_string()));
622
623        // Encoding is the inverse of decoding: the human-readable JSON re-encodes to
624        // exactly the original BCS bytes.
625        let reencoded = formats.encode_operation(&decoded).unwrap();
626        assert_eq!(reencoded, bytes);
627    }
628
629    #[test]
630    fn prune_rejects_colliding_format() {
631        use serde_reflection::ContainerFormat;
632
633        // A registry where `CryptoHash` is (wrongly) bound to a different format.
634        let mut registry = Registry::new();
635        registry.insert(
636            "CryptoHash".to_string(),
637            ContainerFormat::NewTypeStruct(Box::new(Format::U64)),
638        );
639        let unit = Format::Unit;
640        let mut formats = Formats {
641            registry,
642            operation: Format::TypeName("CryptoHash".to_string()),
643            response: unit.clone(),
644            message: unit.clone(),
645            event_value: unit,
646        };
647
648        let error = formats.prune_known_primitives().unwrap_err();
649        assert!(matches!(error, PruneError::Mismatch { name } if name == "CryptoHash"));
650        // The registry is left untouched on mismatch.
651        assert!(formats.registry.contains_key("CryptoHash"));
652    }
653
654    #[test]
655    fn stable_enum_round_trip() {
656        use linera_sdk_derive::StableEnumInCrate;
657        use serde_reflection::ContainerFormat;
658
659        #[derive(Debug, PartialEq, StableEnumInCrate)]
660        enum Op {
661            Increment,
662            Set { value: u64 },
663            Add(i64, i64),
664            Echo(String),
665        }
666
667        // Wire format: each variant tag is exactly 4 ULEB128 bytes.
668        for c in [
669            Op::Increment,
670            Op::Set { value: 99 },
671            Op::Add(2, 3),
672            Op::Echo("hi".into()),
673        ] {
674            let bytes = bcs::to_bytes(&c).unwrap();
675            assert!(bytes.len() >= 4, "tag must be at least 4 bytes: {c:?}");
676            // 4-byte ULEB128: first 3 bytes have continuation bit, 4th doesn't.
677            assert_eq!(bytes[0] & 0x80, 0x80, "byte 0 has continuation");
678            assert_eq!(bytes[1] & 0x80, 0x80, "byte 1 has continuation");
679            assert_eq!(bytes[2] & 0x80, 0x80, "byte 2 has continuation");
680            assert_eq!(bytes[3] & 0x80, 0x00, "byte 3 terminates");
681
682            let back: Op = bcs::from_bytes(&bytes).unwrap();
683            assert_eq!(back, c);
684        }
685
686        // Unknown tags must be rejected.
687        let bogus = bcs::to_bytes(&0u32).unwrap();
688        assert!(bcs::from_bytes::<Op>(&bogus).is_err());
689
690        // Tags published in the trait const match what BCS actually emits.
691        for &(name, tag) in <Op as StableEnumTrace>::STABLE_VARIANTS {
692            let sample = match name {
693                "Increment" => bcs::to_bytes(&Op::Increment).unwrap(),
694                "Set" => bcs::to_bytes(&Op::Set { value: 0 }).unwrap(),
695                "Add" => bcs::to_bytes(&Op::Add(0, 0)).unwrap(),
696                "Echo" => bcs::to_bytes(&Op::Echo(String::new())).unwrap(),
697                _ => unreachable!(),
698            };
699            let decoded = decode_uleb_u32(&sample[..4]);
700            assert_eq!(decoded, tag, "variant {name} tag mismatch");
701        }
702
703        // Tracing via the extension trait records the Keccak tags in the registry.
704        let mut tracer = Tracer::new(TracerConfig::default());
705        let samples = Samples::new();
706        let format = tracer.trace_stable_enum_type::<Op>(&samples).unwrap();
707        let registry = tracer.registry().unwrap();
708        match registry.get("Op").unwrap() {
709            ContainerFormat::Enum(variants) => {
710                let mut keys: Vec<_> = variants.keys().copied().collect();
711                keys.sort();
712                let mut expected: Vec<u32> = <Op as StableEnumTrace>::STABLE_VARIANTS
713                    .iter()
714                    .map(|(_, t)| *t)
715                    .collect();
716                expected.sort();
717                assert_eq!(keys, expected);
718            }
719            _ => panic!("expected enum"),
720        }
721
722        // End-to-end: bcs_to_json works against the reflected registry.
723        let bytes = bcs::to_bytes(&Op::Set { value: 99 }).unwrap();
724        let value = bcs_to_json(&bytes, &format, &registry).unwrap();
725        assert_eq!(value, json!({ "Set": { "value": 99 } }));
726    }
727
728    /// Decode a 4-byte (exactly) ULEB128 sequence into a `u32`.
729    fn decode_uleb_u32(bytes: &[u8]) -> u32 {
730        let b0 = (bytes[0] & 0x7f) as u32;
731        let b1 = (bytes[1] & 0x7f) as u32;
732        let b2 = (bytes[2] & 0x7f) as u32;
733        let b3 = bytes[3] as u32;
734        b0 | (b1 << 7) | (b2 << 14) | (b3 << 21)
735    }
736}