Skip to content

Route admin Configure* messages via DAL + unblock sandbox smoke#3

Open
saroupille wants to merge 8 commits intotrilitech:mainfrom
saroupille:fix/configure-messages-via-dal
Open

Route admin Configure* messages via DAL + unblock sandbox smoke#3
saroupille wants to merge 8 commits intotrilitech:mainfrom
saroupille:fix/configure-messages-via-dal

Conversation

@saroupille
Copy link
Copy Markdown

@saroupille saroupille commented Apr 20, 2026

Summary

Restores a working path for admin configuration messages and re-enables the E2E sandbox smoke against current Octez + protocol Alpha. Three regressions introduced in recent commits are addressed; the first (admin config size) is the central one, the others are sandbox-side catch-up. Adds a variant-exhaustive regression test so the next size regression fails at compile time rather than at deploy time.

Bug 1 — ConfigureVerifier / ConfigureBridge exceed the L1 inbox size limit (central)

Symptom. Since 2c45d9c ("Harden rollup config and operator auth") added WOTS signatures to the two admin messages, their serialized size grew past sc_rollup_message_size_limit = 4096 (Tezos protocol constant):

Message Serialized size (bytes)
ConfigureVerifier 4923
ConfigureBridge 4835

octez-client send smart rollup message enforces this limit and refuses to inject. Admin config is therefore unreachable via the direct L1 inbox path — the only path previously used. No test caught this at merge time (the 288 unit tests don't exercise the L1 path; the E2E sandbox smoke was broken for unrelated reasons and masked the issue).

Fix (commit eae8c6a). Extend the existing DAL routing (already used for large Shield/Transfer/Unshield payloads) to cover both admin variants:

  • core/src/kernel_wire.rs
    • KernelDalPayloadKind gains ConfigureVerifier (wire tag 3) and ConfigureBridge (wire tag 4). A comment clarifies that these tags are independent of WireKernelInboxMessage tags.
    • KERNEL_WIRE_VERSION bumped 9 → 10, so older clients that see a DalPointer with kind ∈ {3, 4} get a clean envelope-version error instead of an opaque tag-decode failure.
    • Size sentinels configure_verifier_serialized_size_sentinel (4923) and configure_bridge_serialized_size_sentinel (4835): they assert > 4096 today and fail loudly if the encoding drifts, forcing a review of DAL-routing assumptions.
  • tezos/rollup-kernel/src/lib.rs
    • fetch_kernel_message_from_dal handles the two new kinds. Dispatch match is now compile-exhaustive on KernelDalPayloadKind (no _ arm): any future variant is a compile error until handled here.
    • Docstring explains the kind-vs-content check is defense-in-depth; authenticity comes from the WOTS signature / STARK proof inside the payload — DAL is a public bulletin board.
  • tezos/rollup-kernel/src/bin/octez_kernel_message.rs
    • configure-verifier-payload and configure-bridge-payload subcommands emit the raw unframed hex (the input to chunking + DAL publish).
    • dal-pointer accepts configure_verifier and configure_bridge kind tokens.
    • When TZEL_ROLLUP_CONFIG_ADMIN_ASK_HEX is unset in debug builds, the fallback to the public dev ask now emits an eprintln! warning.
  • tezos/rollup-kernel/build.rs (new)
    • Emits cargo:rerun-if-env-changed= for the three admin env vars read via option_env!(). Without this, cargo's fingerprint misses env-var changes → rotating admin material can silently reuse a cached WASM baked with the old pub_seed → kernel silently rejects every admin signature. The build.rs doc block describes this failure mode end-to-end.

Operator and wallet-server are intentionally left unchanged. By design, admin config flows directly from an admin's octez_kernel_message + octez-client (admin's own L1 key + WOTS ask), never through the user-facing operator API. This keeps the operator surface narrow, preserves admin availability independent of operator health, and prevents a bearer-token leak from granting the ability to inject admin configs.

Documented end-to-end in tezos/rollup-kernel/README.md (commit 1a95bd5).

Bug 2 — Sandbox smoke broken on current Octez / protocol

Three independent breakages in the same smoke script (scripts/octez_rollup_sandbox_dal_smoke.sh), all surfaced while bringing up the Bug 1 repro. Commit aa31ccd.

  1. attestation_lags vs attestation_lag. Recent protocol Alpha validates that dal_parametric.attestation_lags (plural, per-level list) contains the attestation_lag (singular, sandbox constant). Default mockup ships [1, 2, 3, 4, 5]; our sandbox sets attestation_lag = 2 for speed. Protocol activation errors with a size mismatch. Fix: force attestation_lags = [attestation_lag] during build_alpha_sandbox_params.

  2. Admin material not baked into kernel WASM. prepare_rollup_config_admin.sh generates a runtime ask plus the build-time env vars the kernel bakes via option_env!(). The smoke never called it, so release-profile kernels were built with compiled_config_admin_pub_seed() == Err(_) and silently rejected every admin signature. Fix: call prepare_rollup_config_admin.sh in build_kernel_and_tools and source the env files before the kernel build (with set -a bracketing, commented with a "do not copy to production runners" caveat).

  3. xxd -ps -c 0 wraps at 60 chars. The xxd shipped with current vim-common ignores -c 0 and wraps output at 60 hex chars. Embedded newlines corrupted the hex payloads passed to octez-client send smart rollup message. Fix: pipe through | tr -d '\n' in all three call sites (await_bridge_ticketer, deposit_to_bridge, and the main balance-key computation).

Bug 3 — Sandbox catch-up for bde1347 (apply_shield producer notes)

Two smaller regressions from bde1347 ("Add burned rollup fees and DAL producer note outputs") that were masked by Bug 1 (the smoke never got past configure-verifier). Commit a60c929:

  • apply_shield now debits v + fee + producer_fee from the sender's balance (was v only). Fixture metadata exposed only shield_amount = fixture.shield.v, so the sandbox deposit fell short by fee + producer_fee. Renamed the metadata field to shield_bridge_deposit and compute it as v + fee + producer_fee (500_001 mutez instead of 400_000 for the checked-in fixture).
  • apply_shield appends 2 notes per shield (cm + producer_cm). Sandbox still asserted tree_size == 1. Updated to 2.

The tutorial in docs/shadownet_tutorial.md was affected by the same math mismatch: the shield step deposited 300000 mutez and shielded 200000 mutez, which covered v + burn exactly but left the shield short by the DAL-producer fee. Bumped deposit to 300001 with an explanatory paragraph (commit 3412e0c).

Regression prevention — variant-exhaustive framed-size invariant

Commit 7c08ede adds inbox_size_invariant_covers_all_variants in core/src/kernel_wire.rs::tests. It classifies each KernelInboxMessage variant as FitsL1 or RequiresDal via an exhaustive match (no _ arm), then asserts that the framed on-wire size — unframed bytes + 21 B ExternalMessageFrame::Targetted overhead — matches the classification. Two-sided: a FitsL1 variant growing past 4096 fails; a RequiresDal variant shrinking below 4096 fails too (dead DAL plumbing).

This closes the hole that let 2c45d9c break admin config silently. The existing 288 unit tests didn't exercise the size invariant, and the two size sentinels (configure_*_serialized_size_sentinel) had to be written after the regression surfaced. The new test is structural — any future variant author is forced, at compile time, to classify the new variant before merge.

The sentinels are retained on top of the invariant: they pin exact byte counts (4923, 4835) and catch sub-threshold drift that the invariant's > 4096 bound would miss (e.g. a new optional field in KernelVerifierConfig that shifts size from 4923 → 4987 without crossing any limit).

Code cleanup (adversarial review follow-ups landed in-PR)

  • refactor(kernel): the five near-identical arms of the DAL kind-vs-content dispatcher in fetch_kernel_message_from_dal are collapsed into a single boolean check plus one shared error branch. Compile-time exhaustiveness on KernelDalPayloadKind is preserved (no _ arm), so any future variant still compile-breaks until classified. Net -37 / +18 lines, same behaviour, same error message. Commit 8515b32.
  • chore(wallet, kernel-readme): kernel_message_kind no longer folds admin Configure* messages into RollupSubmissionKind::Withdraw (they are now unreachable!() with a pointer to the admin CLI); the kernel README step 3 of "Adding a new oversized message type" is rewritten to split user-facing payloads (via operator) from admin-signed payloads (direct octez_kernel_message + octez-client) and cross-reference the new variant-exhaustive test. Commit 390aaaa.

Verification

Full E2E smoke passes end-to-end on current Octez (24.0~rc1+dev) + protocol Alpha:

$ ./scripts/octez_rollup_sandbox_dal_smoke.sh
...
octez rollup sandbox DAL smoke passed
rollup=sr18J7twH6WGpUESoEaPgDLpD1dDfweaPpGs
ticketer=KT1XVsMuAAFuy3Pk85ND8a3mpxoezDsGkRZW

Full flow exercised: configure-verifier via DAL → configure-bridge via DAL → bridge deposit → public balance credit → shield via DAL → public balance drain → shielded note insertion (tree size = 2).

cargo test --workspace --lib → 288 passed (+ 1 new test).

Test plan

  • cargo test -p tzel-core --lib passes (72 tests; includes the new invariant + the two existing sentinels)
  • cargo build --workspace exit 0
  • ./scripts/octez_rollup_sandbox_dal_smoke.sh passes end-to-end on current Octez
  • CI workflow runs once CI is unblocked by fix(ci): repair OCaml and cross-impl interop jobs #2

Open questions / follow-ups

Open questions (not addressed in this PR, need a team call):

  • scripts/octez_rollup_sandbox_smoke.sh (non-DAL) and its Rust wrapper tezos/rollup-kernel/tests/octez_sandbox.rs are broken by Bug 1 — their only flow is send_configure_bridge_message via direct L1 inbox, which now exceeds 4096 bytes. The DAL smoke exercises a strict superset (configure-bridge + configure-verifier + deposit + shield). Question for reviewers: delete the non-DAL smoke (redundant), migrate it to DAL (duplicates the DAL smoke), or keep as a documentation-only marker?

  • scripts/shadownet_live_e2e_smoke.sh (the live shadownet smoke) is broken by both Bug 1 and Bug 3:

    • init_profile() does not pass --dal-fee / --dal-fee-address to init-shadownet, which became required arguments.
    • Even with dal_fee = 1, the hardcoded TZEL_SMOKE_DEPOSIT_AMOUNT = 300000 / TZEL_SMOKE_SHIELD_AMOUNT = 200000 leaves the shield debit (200000 + 100000 + 1 = 300001) short by 1 mutez.
      The fix is more invasive than the sandbox fix (needs a generated DAL producer address + operator fee-policy alignment), so tracking as a separate follow-up rather than expanding this PR's scope.

Follow-ups (not blocking this PR):

  • Path-scoped DAL smoke job in .github/workflows/unit-tests.yml (triggered on changes to core/src/kernel_wire.rs, tezos/rollup-kernel/src/**, scripts/octez_rollup_sandbox_dal_smoke.sh). Not added here because CI is currently red on main; fix(ci): repair OCaml and cross-impl interop jobs #2 unblocks it.
  • WOTS one-time property (reusing (ask, key_idx) leaks the private signing key) should be documented explicitly in the kernel README before any production signing workflow.
  • Noise from Dal_attested_slots internal messages mis-routed through decode_kernel_inbox_message fallback (tezos/rollup-kernel/src/lib.rs:569) — harmless decode-error log spam, root cause is tezos-smart-rollup-encoding = 0.2.2 lacking protocol tags 4 and 5. Upstream the missing variants or scope the fallback to External messages only.

🤖 Generated with Claude Code

saroupille and others added 8 commits April 21, 2026 00:13
A WOTS-signed `ConfigureVerifier` KernelInboxMessage serializes to
4923 bytes, and `ConfigureBridge` to 4835 bytes.  Both exceed the
Tezos smart-rollup protocol constant `sc_rollup_message_size_limit`
(4096 bytes), so they cannot transit through the L1 external-message
path that `octez-client send smart rollup message` uses.

This commit extends the existing DAL delivery path — already routing
Shield / Transfer / Unshield payloads too large for L1 — to cover the
two admin configuration messages.

Kernel-side:
  - `core/src/kernel_wire.rs`
      * `KernelDalPayloadKind` gains `ConfigureVerifier` (wire tag 3)
        and `ConfigureBridge` (wire tag 4).
      * `kernel_dal_payload_kind_{to,from}_wire` handle both.
      * A comment clarifies that tag numbering here is independent of
        the tags used by `WireKernelInboxMessage`.
      * `KERNEL_WIRE_VERSION` bumped to 10: older clients that read a
        `DalPointer` with `kind=3|4` now see an explicit envelope
        version mismatch rather than an opaque tag error.
  - `tezos/rollup-kernel/src/lib.rs`
      * `fetch_kernel_message_from_dal` accepts the two new kinds.
      * The dispatch match is reshaped to be exhaustive on
        `KernelDalPayloadKind`: any future variant will be a compile
        error until handled here, instead of silently hitting the old
        `_ => Err("kind mismatch")` arm.
      * Docstring explains that the kind-vs-content check is a
        defense-in-depth control (forces honest labeling for auditors)
        and that authenticity itself comes from the WOTS signature /
        STARK proof inside the payload — DAL is a public bulletin
        board with no transport-level authentication.
      * `dal_payload_kind_name` labels the two new variants.

Tooling:
  - `tezos/rollup-kernel/src/bin/octez_kernel_message.rs`
      * New `configure-verifier-payload` and `configure-bridge-payload`
        subcommands emit the raw unframed KernelInboxMessage hex — the
        input for chunking and DAL publication.
      * `dal-pointer` accepts the `configure_verifier` and
        `configure_bridge` kind tokens.
      * When `TZEL_ROLLUP_CONFIG_ADMIN_ASK_HEX` is unset in debug
        builds, the fallback to the public dev ask now emits a
        `eprintln!` warning; silent fallback paired with a
        release-profile kernel built without admin material would be a
        footgun.
  - `tezos/rollup-kernel/build.rs` (new)
      * Emits `cargo:rerun-if-env-changed=` for the three admin
        material env vars consumed by `option_env!()` in the kernel
        source (the config-admin public seed plus the verifier and
        bridge config-admin WOTS leaves), so a rotation of the admin
        material always re-bakes the WASM.

Tests:
  - `core/src/kernel_wire.rs::tests`
      * Size sentinels for `ConfigureVerifier` (4923 bytes) and
        `ConfigureBridge` (4835 bytes).  The tests pass today; they
        fail loudly if a future encoding change shifts either size,
        forcing a review of DAL routing assumptions.

Operator and wallet-server are intentionally left unchanged: by
design, admin config messages flow directly from an admin's
`octez_kernel_message` + `octez-client` (with the admin's own L1 key
and WOTS ask), never through the user-facing operator API.  This keeps
the operator's interface narrow, preserves admin availability
independent of operator health, and prevents a bearer-token leak from
granting the ability to inject admin configs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add an "Admin configuration messages and DAL routing" section to the
rollup-kernel README describing:

  - why `ConfigureVerifier` and `ConfigureBridge` must use DAL (WOTS
    signature bloat pushes each message above 4096 bytes);
  - the delivery flow end-to-end (admin computes unframed payload,
    chunks to DAL, injects a `DalPointer` on L1; kernel reassembles
    pages, verifies the payload hash, decodes, and dispatches);
  - a checklist for adding a new oversized message type in the
    future (wire tag, dispatch arm, CLI subcommand, size sentinel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five fixes bundled to get `octez_rollup_sandbox_dal_smoke.sh` through
configure / deposit on an Octez master built from recent trunk:

1. `attestation_lags` invariant
   The Alpha protocol gained a restriction (tezos master 8499ce19ac,
   2025-12-04) that the last element of `attestation_lags` must equal
   `attestation_lag`.  The mockup generates `attestation_lags =
   [1,2,3,4,5]` by default, and the script overrides `attestation_lag`
   to 2 via `DAL_ATTESTATION_LAG`, so `activate_alpha` fails.  Force
   `attestation_lags = [attestation_lag]` in `build_alpha_sandbox_params`.

2. Configure messages via DAL
   `configure-verifier` (4944 bytes framed) and `configure-bridge`
   (4856 bytes framed) exceed `sc_rollup_message_size_limit = 4096`,
   so the old direct `octez-client send smart rollup message` path
   fails at encoding.  Route both via the DAL delivery path instead,
   using the new `configure-{verifier,bridge}-payload` CLI subcommands
   and a generalized `publish_payload_via_dal_and_inject_pointer`
   helper factored out of `publish_shield_via_dal_and_inject_pointer`.

3. Admin material baked into the release kernel WASM
   The release kernel's `authenticate_{verifier,bridge}_config` only
   accepts admin-signed payloads when the admin leaves are baked in at
   compile time via `TZEL_ROLLUP_CONFIG_ADMIN_*_HEX`.  Without this,
   the kernel silently rejects every configure payload.  Call
   `scripts/prepare_rollup_config_admin.sh` before the kernel build
   and source both the runtime (secret ask) and build (public leaves)
   env files.

4. `xxd -ps -c 0` newline workaround
   On our xxd version `-c 0` still wraps at ~60 characters, inserting
   newlines that silently break string matches and URL / Michelson
   arg construction.  Pipe to `tr -d '\n'` on the affected call sites:
   `await_bridge_ticketer`, `deposit_to_bridge`, and the balance-key
   construction in `main`.  Without this, `await_bridge_ticketer`
   reports "ticketer did not appear" even after the kernel applied
   the configuration.

5. Caveat on `set -a` scope
   A comment makes explicit that exporting
   `TZEL_ROLLUP_CONFIG_ADMIN_ASK_HEX` to every descendant process is
   acceptable in sandbox (ephemeral per-workdir ask) but must not be
   copied to a production runner.

After these fixes, the smoke reaches and applies configure-verifier +
configure-bridge + the initial bridge deposit; the subsequent fixture
shield step is not within the scope of this patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bde1347 ("Add burned rollup fees and DAL producer note outputs")
made two changes the sandbox smoke script never absorbed:

1. apply_shield now debits `v + fee + producer_fee` (not just `v`)
   from the sender's public balance:

       let debit = req.v
           .checked_add(req.fee)
           .and_then(|value| value.checked_add(req.producer_fee))?;
       if bal < debit { return Err("insufficient balance"); }

   The fixture metadata still exposed only `shield_amount: fixture.shield.v`
   and the sandbox deposited exactly that.  Post-bde1347 the balance is
   short by `fee + producer_fee`, the shield fails with "insufficient
   balance", and the public drain never happens.

   For the checked-in fixture (v=400_000, fee=100_000, producer_fee=1
   mutez) the required deposit is 500_001 mutez instead of 400_000.

   Rename `shield_amount` to `shield_bridge_deposit` in `FixtureMetadata`
   and compute it as `v + fee + producer_fee`.  The sandbox script picks
   up the new field and uses the same value for both the bridge deposit
   and the pre-shield balance assertion.

2. apply_shield now appends *two* notes to the Merkle tree per shield:
   the sender's own commitment and the producer's compensation commitment.
   The smoke's post-shield assertion still expected
   `/tzel/v1/state/tree/size == 1` — the pre-fees value — which makes the
   smoke stall at line 698 even though the shield applied cleanly and the
   public balance drained to zero.

   Update the assertion to `2` and leave a comment pointing at
   apply_shield so the next person knows why.

These two regressions were hidden behind an earlier one (configure
messages exceeding sc_rollup_message_size_limit, fixed in 5071e2e on
this branch): the script never got past configure-verifier, so neither
bde1347-induced break ever executed.  Once configures route through
DAL, the smoke advances through the shield and both breaks surface in
sequence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two size sentinels next to this test lock exact byte counts for
`ConfigureVerifier` and `ConfigureBridge`.  They would have caught the
specific regression they target (a WOTS signature change growing those
two messages past `sc_rollup_message_size_limit = 4096`) — but only
because someone knew to write them *after* the regression surfaced.

A more general failure mode: a new field lands on any
`KernelInboxMessage` variant, pushes its serialized size past 4096,
and nothing fails until an operator tries `octez-client send smart
rollup message` against a real node and gets rejected at the L1 inbox.
That is how commit 2c45d9c broke admin config: unit tests all passed;
the break surfaced weeks later in the sandbox smoke.

Add a third test that makes the invariant structural:

  - A `Routing` enum (`FitsL1` / `RequiresDal`) classifies every
    variant.  `required_routing` is an **exhaustive match** on
    `KernelInboxMessage` with no `_` arm — the compiler forces any
    future variant author to classify the new message before the
    crate builds.

  - `framed_len` computes the on-wire size the L1 inbox actually
    sees, i.e. `encode_kernel_inbox_message(...).len() +
    ExternalMessageFrame::Targetted` overhead (21 bytes: 1 tag + 20
    bytes of `SmartRollupHash`).  The existing sentinels measure the
    unframed envelope and under-count by 21 bytes — a message that
    lands just below 4096 unframed can still be rejected on wire.

  - The assertion is two-sided:
      * FitsL1 with `framed > 4096` fails: the L1 routing is broken.
      * RequiresDal with `framed <= 4096` fails: the DAL plumbing
        for that variant is dead code and the classification needs
        revisiting.

Representative instances:
  - `ConfigureVerifier` / `ConfigureBridge`: real WOTS-signed configs
    (same construction as the sentinels).
  - `Shield` / `Transfer` / `Unshield`: built with a 4096-byte
    `proof_bytes` stub — the cheapest size that keeps the RequiresDal
    classification unambiguous without requiring a full STARK proof
    in the test harness.
  - `Withdraw`: small string fields + `u64`, representative of
    production.
  - `DalPointer`: single-chunk pointer, representative of what the
    kernel emits.

The frame overhead is replicated as a local constant rather than
pulled from `tezos-smart-rollup-encoding` at dev-dep time: that crate
pins `tezos_data_encoding = 0.5.2` while `tzel-core` already depends
on `tezos_data_encoding = 0.6`, and introducing both majors into the
test build for a single 21-byte constant is not worth the friction.
The constant is documented with the layout it replicates and
verified empirically against `octez_kernel_message dal-pointer` output
on a real sr1 address.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After bde1347 ("Add burned rollup fees and DAL producer note outputs"),
`apply_shield` debits `v + fee + producer_fee` from the sender's public
rollup balance, not just `v + fee`.  The tutorial instructed readers to
deposit `300000` mutez before shielding `200000` mutez, which covers `v
+ burn (100000)` exactly — leaving the shield short by the configured
DAL-producer fee (`dal_fee = 1` mutez as set by the init-shadownet
example).  The shield step then fails with "insufficient balance" and
the tutorial cannot be completed as written.

Bump the deposit to `300001` mutez and update the expected post-deposit
balance line to match.  The extra paragraph explains the math so the
next reader understands why the deposit is not a round number.

This matches the sandbox smoke fix in 44adaa8, one tree up.  The
live-shadownet smoke script (`scripts/shadownet_live_e2e_smoke.sh`)
needs a similar bump plus `--dal-fee` / `--dal-fee-address` plumbing in
`init_profile`; that is more invasive (producer-address generation +
operator fee-policy alignment) and is tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two small corrections flagged by the 2nd / 3rd adversarial review that
are cheap enough to land in the same PR rather than trailing as issues.

1. `apps/wallet/src/lib.rs::kernel_message_kind` used to fold
   `Withdraw` / `ConfigureVerifier` / `ConfigureBridge` into the same
   `RollupSubmissionKind::Withdraw` arm.  The wallet never submits
   admin `Configure*` messages (they flow through
   `octez_kernel_message` + `octez-client` directly), so that arm was
   dead code — and silently mislabelling an admin message as a
   `Withdraw` would be a hard-to-spot footgun if some future caller
   ever reached it.  Split the arm: `Withdraw` keeps its own
   mapping, admin `Configure*` variants become an `unreachable!()`
   with a message pointing at the admin CLI.

2. `tezos/rollup-kernel/README.md` step 3 of "Adding a new oversized
   message type" still instructed the next contributor to mirror any
   new variant into `RollupSubmissionKind` and the operator's
   submission-matcher — which directly contradicts the design
   established in commit 5071e2e (admin-signed payloads bypass the
   operator on purpose, so a bearer-token leak cannot authorise
   admin injection).  The old text also named a function
   (`submission_kind_matches_message`) that no longer exists.

   Rewrite step 3 to split the decision by submission path
   (user-facing via operator, admin-signed via `octez_kernel_message`
   directly), and add a pointer to the variant-exhaustive size test
   added in 5c308b4 so the next reader understands it will compile-
   break on an unclassified variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`fetch_kernel_message_from_dal` had five near-identical arms that
checked a `KernelDalPayloadKind` tag against the decoded
`KernelInboxMessage` variant and returned the same "payload kind
mismatch" error when they disagreed.

Collapse the pattern into one `match` that produces a boolean
("does pointer.kind match message?") followed by a single early-
return with the shared error message.  The `match pointer.kind` arms
are still exhaustive (no `_ =>`), so any new `KernelDalPayloadKind`
variant added in the future remains a compile error here until it is
classified — the structural guarantee is preserved.

Net: -37 / +18 lines, same behaviour, same error message, same
compile-time exhaustiveness guarantee.

Suggested by the second adversarial review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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