Symptoms
Fresh-genesis devnet restart on 2026-05-27 produces an immediate zeam-fleet-only consensus split. Every zeam container freezes at head_slot ≈ 52–56 while every non-zeam client advances normally. Wall-clock is at slot 845+ across the fleet.
| Client |
head_slot |
current_slot |
Status |
| zeam_8..15 (all 8) |
52–56 |
844 |
stuck |
| ethlambda_0 |
850 |
850 |
live |
| ethlambda_8 |
845 |
849 |
live |
| lantern_0 |
834 |
850 |
live |
| grandine_0 |
845 |
851 |
live |
| gean_0 |
845 |
851 |
live |
The chain network is healthy. Only zeam stops processing blocks.
Root cause (from log)
Starting at zeam's local slot 52, every incoming block-topic gossip message fails the snappyz.decodeWithMax call at pkgs/network/src/ethlibp2p.zig:538 with error.Corrupt:
[error] [network] Error in snappyz decoding the message for
topic=/leanconsensus/12345678/block/ssz_snappy from peer=unknown_peer:
error.Corrupt
Same line, same error, every block, until the operator restarts. validateGossipSnappyHeader (the pre-decode varint guard at line 529) passes — the failure is inside the snappyz decoder itself.
Slots 0–51 decode fine. Slot 52 onward fails. Every zeam container in the fleet hits this at almost exactly the same slot, so it correlates with chain content (a particular block shape) rather than zeam-local state.
What's not the cause
Investigation hints
-
Content correlation. Slot 52 corresponds to roughly when the network starts producing blocks with aggregated-attestation payloads (justified=20 by then, aggregator activity in full swing). Empty/near-empty early blocks decode fine; blocks carrying STARK aggregate proofs do not. The Corrupt error may surface only when the compressed payload exceeds a particular size or shape.
-
Two snappy implementations in tree. build.zig.zon pins both zig_snappy 0.0.5 (imported as snappyz) and another older revision used as snappyz0. The gossip path uses snappyz.decodeWithMax. Worth checking whether the 0.0.5 decoder mishandles a payload shape the 0.0.3 decoder (or upstream Go snappy reference) accepts.
-
Failure is silent past the metric. The decoded byte length never reaches lean_gossip_block_size_bytes — rejection happens before observation — so the symptom is not visible on Prometheus dashboards. Suggest adding a zeam_gossip_decode_failure_total{topic, error} counter so operators see the stall via metrics, not only via head_slot falling behind.
-
Failed dumps. The decode-failure path writes the raw bytes to failedbytes_* files via writeFailedBytes when shouldPersistMalformedDump() returns true (ethlibp2p.zig:544). On the current devnet image the dump is gated by an env var; turning it on for a single replay would give us the exact byte pattern that triggers error.Corrupt.
Acceptance criteria
Severity
Critical for any release deployed to a network where blocks carry aggregated attestations. zeam falls out of consensus within ~50 slots of every fresh-genesis devnet and cannot participate further until the operator intervenes. Found while validating #940; not caused by #940.
Symptoms
Fresh-genesis devnet restart on 2026-05-27 produces an immediate zeam-fleet-only consensus split. Every zeam container freezes at head_slot ≈ 52–56 while every non-zeam client advances normally. Wall-clock is at slot 845+ across the fleet.
The chain network is healthy. Only zeam stops processing blocks.
Root cause (from log)
Starting at zeam's local slot 52, every incoming block-topic gossip message fails the
snappyz.decodeWithMaxcall atpkgs/network/src/ethlibp2p.zig:538witherror.Corrupt:Same line, same error, every block, until the operator restarts.
validateGossipSnappyHeader(the pre-decode varint guard at line 529) passes — the failure is inside the snappyz decoder itself.Slots 0–51 decode fine. Slot 52 onward fails. Every zeam container in the fleet hits this at almost exactly the same slot, so it correlates with chain content (a particular block shape) rather than zeam-local state.
What's not the cause
MAX_GOSSIP_BLOCK_SIZE = 50 MB(ethlibp2p.zig:45) is well above any real block.Investigation hints
Content correlation. Slot 52 corresponds to roughly when the network starts producing blocks with aggregated-attestation payloads (justified=20 by then, aggregator activity in full swing). Empty/near-empty early blocks decode fine; blocks carrying STARK aggregate proofs do not. The Corrupt error may surface only when the compressed payload exceeds a particular size or shape.
Two snappy implementations in tree.
build.zig.zonpins bothzig_snappy 0.0.5(imported assnappyz) and another older revision used assnappyz0. The gossip path usessnappyz.decodeWithMax. Worth checking whether the 0.0.5 decoder mishandles a payload shape the 0.0.3 decoder (or upstream Go snappy reference) accepts.Failure is silent past the metric. The decoded byte length never reaches
lean_gossip_block_size_bytes— rejection happens before observation — so the symptom is not visible on Prometheus dashboards. Suggest adding azeam_gossip_decode_failure_total{topic, error}counter so operators see the stall via metrics, not only viahead_slotfalling behind.Failed dumps. The decode-failure path writes the raw bytes to
failedbytes_*files viawriteFailedByteswhenshouldPersistMalformedDump()returns true (ethlibp2p.zig:544). On the current devnet image the dump is gated by an env var; turning it on for a single replay would give us the exact byte pattern that triggerserror.Corrupt.Acceptance criteria
snappyz.decodeWithMaxreturnerror.Corrupton payloads peers produced correctly.zeam_gossip_decode_failure_total(or equivalent) so this class of stall is detectable from metrics before head_slot drifts.Severity
Critical for any release deployed to a network where blocks carry aggregated attestations. zeam falls out of consensus within ~50 slots of every fresh-genesis devnet and cannot participate further until the operator intervenes. Found while validating #940; not caused by #940.