Route admin Configure* messages via DAL + unblock sandbox smoke#3
Open
saroupille wants to merge 8 commits intotrilitech:mainfrom
Open
Route admin Configure* messages via DAL + unblock sandbox smoke#3saroupille wants to merge 8 commits intotrilitech:mainfrom
saroupille wants to merge 8 commits intotrilitech:mainfrom
Conversation
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>
0cd0f53 to
8515b32
Compare
This was referenced Apr 21, 2026
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.
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/ConfigureBridgeexceed 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 pastsc_rollup_message_size_limit = 4096(Tezos protocol constant):ConfigureVerifierConfigureBridgeoctez-client send smart rollup messageenforces 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.rsKernelDalPayloadKindgainsConfigureVerifier(wire tag 3) andConfigureBridge(wire tag 4). A comment clarifies that these tags are independent ofWireKernelInboxMessagetags.KERNEL_WIRE_VERSIONbumped 9 → 10, so older clients that see aDalPointerwithkind ∈ {3, 4}get a clean envelope-version error instead of an opaque tag-decode failure.configure_verifier_serialized_size_sentinel(4923) andconfigure_bridge_serialized_size_sentinel(4835): they assert> 4096today and fail loudly if the encoding drifts, forcing a review of DAL-routing assumptions.tezos/rollup-kernel/src/lib.rsfetch_kernel_message_from_dalhandles the two new kinds. Dispatchmatchis now compile-exhaustive onKernelDalPayloadKind(no_arm): any future variant is a compile error until handled here.tezos/rollup-kernel/src/bin/octez_kernel_message.rsconfigure-verifier-payloadandconfigure-bridge-payloadsubcommands emit the raw unframed hex (the input to chunking + DAL publish).dal-pointeracceptsconfigure_verifierandconfigure_bridgekind tokens.TZEL_ROLLUP_CONFIG_ADMIN_ASK_HEXis unset in debug builds, the fallback to the public dev ask now emits aneprintln!warning.tezos/rollup-kernel/build.rs(new)cargo:rerun-if-env-changed=for the three admin env vars read viaoption_env!(). Without this, cargo's fingerprint misses env-var changes → rotating admin material can silently reuse a cached WASM baked with the oldpub_seed→ kernel silently rejects every admin signature. Thebuild.rsdoc 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(commit1a95bd5).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. Commitaa31ccd.attestation_lagsvsattestation_lag. Recent protocol Alpha validates thatdal_parametric.attestation_lags(plural, per-level list) contains theattestation_lag(singular, sandbox constant). Default mockup ships[1, 2, 3, 4, 5]; our sandbox setsattestation_lag = 2for speed. Protocol activation errors with a size mismatch. Fix: forceattestation_lags = [attestation_lag]duringbuild_alpha_sandbox_params.Admin material not baked into kernel WASM.
prepare_rollup_config_admin.shgenerates a runtimeaskplus the build-time env vars the kernel bakes viaoption_env!(). The smoke never called it, so release-profile kernels were built withcompiled_config_admin_pub_seed() == Err(_)and silently rejected every admin signature. Fix: callprepare_rollup_config_admin.shinbuild_kernel_and_toolsand source the env files before the kernel build (withset -abracketing, commented with a "do not copy to production runners" caveat).xxd -ps -c 0wraps at 60 chars. Thexxdshipped with currentvim-commonignores-c 0and wraps output at 60 hex chars. Embedded newlines corrupted the hex payloads passed tooctez-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_shieldproducer 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 pastconfigure-verifier). Commita60c929:apply_shieldnow debitsv + fee + producer_feefrom the sender's balance (wasvonly). Fixture metadata exposed onlyshield_amount = fixture.shield.v, so the sandbox deposit fell short byfee + producer_fee. Renamed the metadata field toshield_bridge_depositand compute it asv + fee + producer_fee(500_001 mutez instead of 400_000 for the checked-in fixture).apply_shieldappends 2 notes per shield (cm + producer_cm). Sandbox still assertedtree_size == 1. Updated to2.The tutorial in
docs/shadownet_tutorial.mdwas affected by the same math mismatch: the shield step deposited300000mutez and shielded200000mutez, which coveredv + burnexactly but left the shield short by the DAL-producer fee. Bumped deposit to300001with an explanatory paragraph (commit3412e0c).Regression prevention — variant-exhaustive framed-size invariant
Commit
7c08edeaddsinbox_size_invariant_covers_all_variantsincore/src/kernel_wire.rs::tests. It classifies eachKernelInboxMessagevariant asFitsL1orRequiresDalvia an exhaustive match (no_arm), then asserts that the framed on-wire size — unframed bytes + 21 BExternalMessageFrame::Targettedoverhead — matches the classification. Two-sided: aFitsL1variant growing past 4096 fails; aRequiresDalvariant shrinking below 4096 fails too (dead DAL plumbing).This closes the hole that let
2c45d9cbreak 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
> 4096bound would miss (e.g. a new optional field inKernelVerifierConfigthat 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 infetch_kernel_message_from_dalare collapsed into a single boolean check plus one shared error branch. Compile-time exhaustiveness onKernelDalPayloadKindis preserved (no_arm), so any future variant still compile-breaks until classified. Net -37 / +18 lines, same behaviour, same error message. Commit8515b32.chore(wallet, kernel-readme):kernel_message_kindno longer folds adminConfigure*messages intoRollupSubmissionKind::Withdraw(they are nowunreachable!()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 (directoctez_kernel_message+octez-client) and cross-reference the new variant-exhaustive test. Commit390aaaa.Verification
Full E2E smoke passes end-to-end on current Octez (
24.0~rc1+dev) + protocol Alpha: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 --libpasses (72 tests; includes the new invariant + the two existing sentinels)cargo build --workspaceexit 0./scripts/octez_rollup_sandbox_dal_smoke.shpasses end-to-end on current OctezOpen 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 wrappertezos/rollup-kernel/tests/octez_sandbox.rsare broken by Bug 1 — their only flow issend_configure_bridge_messagevia 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-addresstoinit-shadownet, which became required arguments.dal_fee = 1, the hardcodedTZEL_SMOKE_DEPOSIT_AMOUNT = 300000/TZEL_SMOKE_SHIELD_AMOUNT = 200000leaves 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):
.github/workflows/unit-tests.yml(triggered on changes tocore/src/kernel_wire.rs,tezos/rollup-kernel/src/**,scripts/octez_rollup_sandbox_dal_smoke.sh). Not added here because CI is currently red onmain; fix(ci): repair OCaml and cross-impl interop jobs #2 unblocks it.(ask, key_idx)leaks the private signing key) should be documented explicitly in the kernel README before any production signing workflow.Dal_attested_slotsinternal messages mis-routed throughdecode_kernel_inbox_messagefallback (tezos/rollup-kernel/src/lib.rs:569) — harmless decode-error log spam, root cause istezos-smart-rollup-encoding = 0.2.2lacking protocol tags 4 and 5. Upstream the missing variants or scope the fallback to External messages only.🤖 Generated with Claude Code