Skip to content

fix(solidity): emit bool primitive helpers for Option types#100

Merged
ma2bd merged 1 commit into
zefchain:mainfrom
deuszx:fix/solidity-option-bool-helpers
Jun 17, 2026
Merged

fix(solidity): emit bool primitive helpers for Option types#100
ma2bd merged 1 commit into
zefchain:mainfrom
deuszx:fix/solidity-option-bool-helpers

Conversation

@deuszx

@deuszx deuszx commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

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.

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.
@deuszx deuszx requested a review from ma2bd as a code owner June 17, 2026 13:01
@ma2bd ma2bd merged commit 870f819 into zefchain:main Jun 17, 2026
14 checks passed
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)
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.

2 participants