Skip to content

ABIs: derive StableEnum for all ContractAbi Operation/Response enums#6314

Draft
ma2bd wants to merge 4 commits into
ma2bd/stable_enumfrom
ma2bd/abi-stable-enum
Draft

ABIs: derive StableEnum for all ContractAbi Operation/Response enums#6314
ma2bd wants to merge 4 commits into
ma2bd/stable_enumfrom
ma2bd/abi-stable-enum

Conversation

@ma2bd
Copy link
Copy Markdown
Contributor

@ma2bd ma2bd commented May 15, 2026

Motivation

Follow-up to #6309: now that #[derive(StableEnum)] exists, switch every ContractAbi operation/response enum in the repo to use it. This locks each variant tag to a Keccak-derived 4-byte ULEB128 value, decoupling the wire format from declaration order so reorderings/insertions don't silently break consumers.

Proposal

Apply #[derive(StableEnum)] (or StableEnumInCrate for code inside linera-sdk) to every Operation and enum-typed Response:

  • Example contracts: amm, call-evm-counter, counter-no-graphql, crowd-funding, ethereum-tracker, evm-bridge (via linera-bridge), gen-nft, hex-game (both Operation and the HexOutcome response), matching-engine, non-fungible, rfq, social, task-processor, how-to/perform-http-requests.
  • In-crate linera-sdk ABIs: fungible::FungibleOperation + FungibleResponse, controller::Operation, wrapped_fungible::WrappedFungibleOperation.
  • linera-sdk fixtures: contract-call, cost-tracking, publish-read-data-blob, event-emitter, create-and-call, time-expiry, event-subscriber.

Other changes pulled in:

  • linera-bridge gets a direct (non-optional) linera-sdk dependency so BridgeOperation can use the derive. This was already a transitive dep via wrapped-fungible in the relay feature.
  • Each formats module now traces the stable enum via tracer.trace_stable_enum_type::<T>(&samples)? (importing TracerExt), since Tracer::trace_type cannot enumerate the non-contiguous Keccak tags.
  • Removed unused scalar!(Operation) calls in amm and rfq. scalar! registers an async-graphql ScalarType impl backed by serde_json, but StableEnum's serde impls use a numeric variant tag that serde_json cannot deserialize from a string variant name. Nothing in the codebase actually invokes them — GraphQLMutationRoot exposes individual fields rather than the whole enum — so the impls are dead code that would silently break if exercised.

Skipped because they have no enum to convert: evm.rs (Operation = Vec<u8>), llm (Operation = ()), meta-counter (Operation is a struct), track-instantiation (Operation = ()).

Test Plan

  • cargo check --workspace and cargo check on the examples workspace.
  • All 18 example format snapshot tests regenerated with the new Keccak tags and pass.
  • linera-sdk's formats::tests (including stable_enum_round_trip) pass.
  • cargo +nightly fmt --check and cargo clippy --workspace --all-targets are clean.

Release Plan

  • Nothing to do / These changes follow the usual release cycle.

Links

Apply `#[derive(StableEnum)]` to every contract's `Operation` (and any
`Response` that is an enum) across the example contracts, the in-crate
`linera-sdk` ABIs (fungible, controller, wrapped-fungible), the
fixtures used by the SDK integration tests, and the EVM bridge's
`BridgeOperation` (which lives in `linera-bridge`, now a direct
`linera-sdk` dependent — already transitive via `wrapped-fungible`).

Format snapshots regenerated. Dead `scalar!(Operation)` calls in `amm`
and `rfq` removed: the macro provides a JSON `ScalarType` impl that
would silently fail because StableEnum's 4-byte ULEB128 variant tags
are not JSON-deserializable; nothing in the codebase actually invoked
them since `GraphQLMutationRoot` exposes individual fields rather than
the whole enum.
@ma2bd ma2bd marked this pull request as draft May 15, 2026 05:43
ma2bd added 3 commits May 16, 2026 07:43
The previous commit applied `#[derive(StableEnum)]` to `WrappedFungibleOperation`
(and other bridge-touching enums), which changes the BCS wire format: each
variant tag becomes a 4-byte ULEB128 encoding of a Keccak-derived `u32`, not
a sequential `uint8` index.

The `serde-generate` Solidity backend on crates.io (0.33.0) only emits
single-byte `uint8` discriminants, so the auto-generated `BridgeTypes.sol`
and `WrappedFungibleTypes.sol` would silently produce 1-byte tags that the
Wasm side would reject. While `linera-bridge`'s relay doesn't currently
*construct* `WrappedFungibleOperation` from Solidity, the codegen is run via
the `codegen` feature and we want the output to match Wasm's wire format
out of the box for future EVM-side callers.

Pin `serde-generate` and `serde-reflection` to the zefchain fork commit
8882df9 (added to `[patch.crates-io]`), which:
  * widens the `choice` discriminant from `uint8` to `uint64`,
  * emits each variant's BCS prefix as a precomputed `hex"..."` literal
    (handles both sequential and non-contiguous tags), and
  * decodes the prefix via `bcs_deserialize_offset_len` (ULEB128) instead
    of a single `uint8` read.

Also fix `format_wrapped_fungible.rs`: trace `WrappedFungibleOperation` via
`tracer.trace_stable_enum_type` (which walks the enum variant-by-variant)
instead of `trace_type` (which probes contiguous indices and never
terminates on Keccak tags). Regenerate the snapshot accordingly, which is
what now drives the codegen to emit the correct stable tags.

`BridgeTypes.sol` is also regenerated; the discriminant widening is the
only material change for its (non-StableEnum) enums. Hand-written callers
(`FungibleBridge.sol`, `LightClient.sol`) compare `choice` against integer
literals so they remain compatible with the wider type.
`#[derive(StableEnum)]` references `serde` via the
`linera_sdk::formats::__private::serde` re-export, so the four converted
crates (event-emitter, event-subscriber, time-expiry, and the
how-to-perform-http-requests example) no longer need a direct dependency
on `serde`. cargo-machete flagged them.
* `evm_call_wasm_example_counter.sol`: this is a hand-written fixture
  (not generated by `serde-generate`), so the codegen patch from the
  previous commit doesn't reach it. Convert `CounterOperation`'s tag
  from the old `uint8(0)` to the 4-byte ULEB128 stable tag
  `BF D5 87 59` that matches `Keccak256("Increment")[..4]`, the format
  emitted by `#[derive(StableEnum)]`. Without this, the REVM test
  `test_evm_call_wasm_end_to_end_counter` fails with a Wasm trap on
  Operation deserialization.
* `linera-sdk/tests/fixtures/Cargo.lock`: regenerate after dropping the
  unused `serde` direct dep from event-emitter, event-subscriber, and
  time-expiry. This is a separate workspace from the top-level one, so
  its lockfile needs an independent refresh — the CI step that runs
  `cargo test --locked` in that directory was failing.
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