Skip to content

linera-sdk: derive macro StableEnum (backport of #6309) - #6543

Draft
ma2bd wants to merge 2 commits into
testnet_conwayfrom
ma2bd/backport-6309-conway
Draft

linera-sdk: derive macro StableEnum (backport of #6309)#6543
ma2bd wants to merge 2 commits into
testnet_conwayfrom
ma2bd/backport-6309-conway

Conversation

@ma2bd

@ma2bd ma2bd commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Motivation

Backport of #6309 to testnet_conway. This is the prerequisite for backporting #6314 (apply StableEnum to all ABI Operation/Response enums), which the release plan calls for.

Proposal

Cherry-pick #6309, which adds the #[derive(StableEnum)] / StableEnumInCrate macros to linera-sdk-derive plus the companion StableEnumTrace/StableEnum traits and TracerExt::trace_stable_enum_type in linera-sdk::formats. Stable enums lock each variant tag to a 4-byte Keccak-derived ULEB128 value, decoupling the wire format from declaration order.

Conway-specific adaptations

The cherry-pick did not apply cleanly because conway's counter example still uses type Operation = u64 — the CounterOperation enum on main was introduced by the unrelated #6264 (application introspection), which is not on testnet_conway. I applied only the operation-enum migration that #6309 depends on, leaving conway's other differences (ANCHOR docs, QueryRoot, non-Arc service state) untouched:

  • examples/counter (lib.rs, contract.rs, service.rs, tests/single_chain.rs): migrate the operation type from u64 to CounterOperation::Increment { value } and add #[derive(StableEnum)].
  • linera-sdk/tests/fixtures/meta-counter/src/contract.rs and linera-core/src/unit_tests/wasm_client_tests.rs: wrap the cross-application / user operations in CounterOperation::Increment so the wire format matches the migrated contract.
  • Lockfiles refreshed for the new sha3 dependency edge on linera-sdk-derive (root, examples, and linera-sdk/tests/fixtures).

Test Plan

  • linera-sdk unit tests pass, including formats::tests::stable_enum_round_trip.
  • All counter example tests pass, including the format snapshot test (regenerated Keccak tag) and the single_chain end-to-end test (real wasm round-trip of CounterOperation).
  • cargo check -p linera-core --tests --features wasmer compiles (covers wasm_client_tests and the meta-counter fixture).
  • cargo +nightly fmt --check and cargo clippy clean on the root, examples, and fixtures workspaces; all three lockfiles verified --locked-consistent.

Release Plan

  • These changes should be backported to the latest testnet branch (this PR targets testnet_conway).

Links

ma2bd and others added 2 commits June 17, 2026 04:41
We want enum variant tags on the wire to be stable under variant
reordering, similar to Solidity's 4-byte function selectors.
* Today, BCS serializes the serde variant *index* (0, 1, 2, …,
ULEB128-encoded), which changes whenever variants are reordered.
* This is especially a problem when we want an ABI to extend several
existing ones. (Typically different flavors of `fungible`). After this
PR, we could occasionally merge enums with solutions like
https://github.com/rafaeltheraven/extendable-data.

Add a `#[derive(StableEnum)]` macro in `linera-sdk-derive` that emits,
in one shot:

* `Serialize` / `Deserialize` impls calling `serialize_*_variant(name,
tag, …)` / matching `tag` from `EnumAccess`, where the tag is derived at
compile time from the variant name:
  ```
  tag = (be_u32(Keccak-256(name)[..4]) & 0x07FF_FFFF) | 0x0800_0000
  ```
Masking the top 5 bits to `00001` constrains the tag to `[2^27, 2^28)`,
which is exactly the range whose ULEB128 encoding is **always 4 bytes**
— so every variant tag on the wire takes exactly 4 bytes.
* A `StableEnumTrace` impl exposing the `(name, tag)` pairs and a
`trace_all_variants` method that drives `serde_reflection::Tracer`
without caller-supplied samples. (The default `Tracer::trace_type`
cannot terminate on non-contiguous u32 indices like ours, so we provide
our own tracing entry point.)

Companion additions in `linera-sdk::formats`:

* `trait StableEnumTrace` (with `const STABLE_VARIANTS: &[(&str, u32)]`
and `fn trace_all_variants`).
* `trait StableEnum: StableEnumTrace + Serialize + DeserializeOwned {}`
blanket-implemented — usable as a single bound.
* `TracerExt::trace_stable_enum_type::<T>(&Samples) -> Result<Format>`
extension method on `Tracer`.
* `StableEnumInCrate` variant of the derive (paths via `crate::…`
instead of `::linera_sdk::…`) for use inside `linera-sdk` itself.

The `StableEnumTrace` impl is `#[cfg(not(target_arch = \"wasm32\"))]` so
applications keep building for wasm without `serde-reflection`.

The `counter` example is migrated as a reference; other examples still
use plain `#[derive(Serialize, Deserialize)]` and can be migrated in
follow-up PRs.

Note: Importantly, the variant index used by Serde is different from the
"discriminant" used by Rust in memory. Assigning new tag values using
`repr(u32)` and the Rust syntax only changes the discriminant.

* New unit test `formats::tests::stable_enum_round_trip` in `linera-sdk`
covering:
  * BCS roundtrip for unit / newtype / tuple / struct variants
* Every variant tag is exactly 4 ULEB128 bytes on the wire
(continuation-bit pattern asserted explicitly)
  * Unknown tags are rejected on deserialization
  * `STABLE_VARIANTS` constant matches what BCS emits
  * `trace_stable_enum_type` populates the registry with hashed keys
  * End-to-end `bcs_to_json` against the reflected registry
* `counter` snapshot test updated: `CounterOperation::Increment` is now
registered under tag `186772159` (`0x0b21eabf`) instead of index `0`.
* All other counter tests (`single_chain`, contract/service unit tests)
continue to pass.

This should backported to `testnet_conway`.

---------

Signed-off-by: Mathieu Baudet <1105398+ma2bd@users.noreply.github.com>
Co-authored-by: Mathieu Baudet <mathieu.baudet@linera.io>
Co-authored-by: Andreas Fackler <afck@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant