Skip to content

All zeam nodes stall on devnet: snappyz.decodeWithMax rejects every block gossip message as error.Corrupt #942

Description

@ch4r10t33r

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

  1. 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.

  2. 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.

  3. 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.

  4. 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

  • Identify the specific byte pattern (or content shape) that makes snappyz.decodeWithMax return error.Corrupt on payloads peers produced correctly.
  • Fix or replace the decode path so zeam can ingest the same gossip messages every other lean-consensus client accepts.
  • Add zeam_gossip_decode_failure_total (or equivalent) so this class of stall is detectable from metrics before head_slot drifts.
  • Regression test on a captured failure-bytes corpus.

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions