fix(solidity): emit bool primitive helpers for Option types#100
Merged
Conversation
The Solidity backend's opt_* (de)serializer reads a one-byte bool tag, and an Option's dependency list already declares "bool" — but insert() never registered Primitive(Bool) in `names` unless some container had a literal bool field. A registry with Option fields but no literal bool therefore generated code that calls bcs_*_bool without ever defining it. Register the bool primitive whenever an Option is inserted (mirrors the existing I8->U8 internal-dependency handling), and add a regression test.
ma2bd
approved these changes
Jun 17, 2026
deuszx
added a commit
to linera-io/linera-protocol
that referenced
this pull request
Jun 17, 2026
## Motivation Linera emits a system epoch event (stream `StreamId::system([0])`) on every committee rotation. Detecting rotations from that event — rather than parsing the `CreateCommittee` operation out of a transaction — lets the lean `BlockProof` (header + signatures) carry committee updates the same way it already carries burns: register the signed block once, then prove a single event's inclusion against it. It also removes `Transaction` (and its whole type subtree) from the EVM-side BCS codegen. ## Proposal - `addCommittee` installs a new committee from a single epoch event proven included in the admin-chain block that created it. `_readCommitteeEvent` requires the event to be on the system epoch stream and reads the new epoch (`event.index`) and committee blob hash (`EpochEventData` payload) from it. - **Unified register-then-prove path.** `addCommittee` no longer carries the full `blockProof` and re-verifies the quorum inline. Instead, the admin block is registered first (`registerBlock`, which checks the quorum and records `events_hash`), and `addCommittee` references it by `blockHash` — exactly as `processBurns` does for burns. Both entrypoints now call the same `proveEventsCommitted` primitive and prove the same thing (an event belongs to a signed block); only the action on success differs. Committee rotation is therefore two transactions (`registerBlock` + `addCommittee`); rotations are rare, so the extra tx is acceptable. - `RegisteredBlock` records the signed block's `epoch`, so `addCommittee` keeps its `block epoch must match current epoch` check (a current-committee-signature guarantee, distinct from `_setCommittee`'s sequential `newEpoch == currentEpoch + 1`) without the inline header. - **Single inclusion witness.** `proveEventsCommitted` takes the sibling hashes as one `bytes32[] siblings` (inner siblings followed by outer, split on-chain at `numEventsInTx - positions.length`) rather than two arrays. With four dynamic calldata arrays the verification call sat one slot past the EVM's 16-slot stack limit (`Variable ... is 1 too deep`, even under `--via-ir`); merging the two sibling arrays into one keeps it compilable. The relay and the contract tests build both `processBurns` and `addCommittee` calls from a single `ProvenEvents` witness (`block_hash` + inclusion proof + event BCS), and `EventInclusionProof::siblings()` produces the concatenation. - Relay: `find_committee_event` locates the system epoch event in the admin-chain block; `add_committee` registers the block then proves the event against it, mirroring the burn-settlement path. - Dropped `Transaction` (and the now-unreachable `Operation`/`Message`/`SystemOperation`/… types) from the `serde-reflection` trace, shrinking generated `BridgeTypes.sol` by ~2,200 lines. The generated `opt_*` (de)serializers read a one-byte bool tag, so dropping the last literal `bool` field left them calling an undefined `bcs_*_bool`; fixed upstream so `serde-generate` emits the bool primitive for `Option` types directly (zefchain/serde-reflection#100, released in `serde-generate` 0.34.1, to which the dependency is bumped), removing the previous `ChainOwnership`-carrier workaround. - Removed the dead `EvmLightClient` duplicate, keeping its validator-key-derivation helpers. Known limitation: the relay relays one rotation per admin-chain block; an admin block carrying two `CreateCommittee` ops (epochs N, N+1) would relay only N. This is a safe halt (the contract reverts rather than installing out-of-order state) and is not attacker-triggerable; iterating all epoch events per block is a follow-up. ## Test Plan - `cargo test -p linera-bridge --features relay` — 145 unit/integration tests (offchain + relay), including revm deploy-and-call coverage of `registerBlock` → `addCommittee`/`processBurns` against the merged-`siblings` ABI and a multi-validator rotation proptest. - `cd linera-bridge/src/solidity && forge test` — 13 Solidity tests; `forge fmt --check` clean. - `cargo clippy -p linera-bridge --features relay --all-targets -- -D warnings`. - The `committee_rotation` e2e test drives a real committee rotation on Linera through the relay to the EVM `LightClient`. - `BridgeTypes.sol` and the format snapshot were regenerated via the `codegen` feature and checked in (CI gates on no drift). ## Release Plan - Nothing to do / These changes follow the usual release cycle. ## Links - Stacked on #6472. - Upstream fix (released in `serde-generate` 0.34.1): zefchain/serde-reflection#100. - [reviewer checklist](https://github.com/linera-io/linera-protocol/blob/main/CONTRIBUTING.md#reviewer-checklist)
ixmoyren
pushed a commit
to ixmoyren/serde-reflection
that referenced
this pull request
Jun 18, 2026
…#100) The Solidity backend generates `opt_*` wrappers for `Option<T>` whose (de)serializer reads a one-byte bool tag and therefore calls `bcs_serialize_bool` / `bcs_deserialize_offset_bool`. An `Option`'s dependency list already declares `"bool"` (so it is in the `needed` set), but `SolRegistry::insert` only registered `Primitive(Bool)` in `names` when some container had a *literal* `bool` field. Result: a registry that uses `Option` fields but has no literal `bool` anywhere generates Solidity that **calls `bcs_*_bool` without ever defining it** — a compile error in the emitted contract. Downstream users currently work around this by tracing a dummy struct that carries a `bool` field solely to anchor the helpers. Fix: register the `bool` primitive whenever an `Option` is inserted, mirroring the existing `I8 -> also-insert-U8` internal-dependency handling in `insert`. `OptionBool` is unaffected (its inner `bool` already inserts the primitive); enums/seqs/maps use uleb128 tags rather than a bool tag, so `Option` is the only case. Added a regression test (`test_option_emits_bool_primitive_without_literal_bool_field`) that traces a struct with an `Option<Inner>` field and no literal `bool`, asserting the generated code defines both bool helpers. It fails before the fix, passes after. Full `solidity` test suite (26 tests, incl. the solc-compiling `test_solidity_compilation`) passes; `cargo fmt`/`clippy` clean. (cherry picked from commit 870f819)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Solidity backend generates
opt_*wrappers forOption<T>whose (de)serializer reads a one-byte bool tag and therefore callsbcs_serialize_bool/bcs_deserialize_offset_bool. AnOption's dependency list already declares"bool"(so it is in theneededset), butSolRegistry::insertonly registeredPrimitive(Bool)innameswhen some container had a literalboolfield.Result: a registry that uses
Optionfields but has no literalboolanywhere generates Solidity that callsbcs_*_boolwithout ever defining it — a compile error in the emitted contract. Downstream users currently work around this by tracing a dummy struct that carries aboolfield solely to anchor the helpers.Fix: register the
boolprimitive whenever anOptionis inserted, mirroring the existingI8 -> also-insert-U8internal-dependency handling ininsert.OptionBoolis unaffected (its innerboolalready inserts the primitive); enums/seqs/maps use uleb128 tags rather than a bool tag, soOptionis the only case.Added a regression test (
test_option_emits_bool_primitive_without_literal_bool_field) that traces a struct with anOption<Inner>field and no literalbool, asserting the generated code defines both bool helpers. It fails before the fix, passes after. Fullsoliditytest suite (26 tests, incl. the solc-compilingtest_solidity_compilation) passes;cargo fmt/clippyclean.