Block Rehearsal
Crashing a Mina validator through a zip_exn that fires before any proof is ever verified.
NNS{ziP_3XN_tuRn5_eX7R4_sNaRK_w0rK_1Nto_4_N0d3_CR4sH!} Challenge
A TCP (TLS) service (service.ml). It reads ONE Staged_ledger_diff.Stable.Latest.t, bin_prot-encoded, sent as lowercase hex. It decodes with bin_read_t (requiring the buffer be fully consumed — else “malformed proposal”), then runs Staged_ledger.apply on a fresh ephemeral ledger with get_completed_work:(fun _ -> None).
Win condition — the server prints validator panicked\n<FLAG> ONLY when apply raises Invalid_argument whose message starts with "length mismatch in zip_exn:" and the diff’s completed_works is non-empty (is_completed_work_pairing_panic + has_extra_completed_work). Every other exception → “validator error”; accept/reject → nothing.
The bug
Staged_ledger.apply calls check_completed_works as its first step, before any diff validation or proof verification:
List.zip_exn (Scan_state.k_work_pairs_for_new_diff scan_state ~k:work_count) completed_works
The server’s ledger/scan-state is freshly created (Scan_state.empty), so k_work_pairs_for_new_diff returns [] regardless of k. Zipping [] against a 1-element completed_works list raises Invalid_argument "length mismatch in zip_exn: 0 <> 1" — exactly the panic the server rewards. No valid proof is needed — the crash happens before any verification. So the whole task reduces to: submit a diff that (a) bin-decodes as the real type and (b) has exactly one (any) completed_work.
Why it’s hard: the serialization
Staged_ledger_diff.Stable.Latest.t is a deeply nested versioned type: diff = (Pre_diff_two, Pre_diff_one option), where Pre_diff_two.completed_works : Transaction_snark_work.Stable.t list, and each work carries proofs : Ledger_proof One_or_two — a full Pickles Proofs_verified_2.Stable.V3 proof. Hand-encoding this bin_prot layout by hand is extremely error-prone (a first attempt kept returning “malformed proposal”).
Robust solution — use Mina’s own derived serializer
Instead of hand-encoding, build a tiny OCaml program linking the real Mina libraries and let the library’s bin_writer_t produce the bytes (cannot have a layout bug):
- Build mina-src in an opam switch with OCaml 4.14.2. Gotcha on Ubuntu 24.04: the vendored Rust/C crate
ocaml-boxroot-sys(viakimchi_bindings) hasstatic int try_free_chunks()(empty param list) called with an argument — illegal under GCC 15’s default-std=gnu23. Fix:export CFLAGS=-std=gnu17beforedune build. - Generator (
gen.ml, added as a dune exe in the mina tree) constructs a real value:diff = ({ completed_works = [one]; commands = []; coinbase = At_most_two.Zero; internal_command_statuses = [] }, None)- the one
Transaction_snark_work.Stable.Latest.tusesLedger_proof.For_tests.mk_dummy_proof(library dummy built fromMina_base.Proof.transaction_dummy, a realProofs_verified_2proof) with aQuickcheck-generatedSnarked_ledger_state.tstatement. - serialize:
Bin_prot.Utils.bin_dump ~header:false Staged_ledger_diff.Stable.Latest.bin_writer_t value→ hex.
- Send the ~11 KB payload over TLS (
send.py). Server repliedvalidator panicked+ the flag on the first attempt.
Files (blockchain/block-rehearsal/)
blockchain_block-rehearsal/gen.ml— payload generator sourceblockchain_block-rehearsal/gen_dune.txt— its dune stanzablockchain_block-rehearsal/build_gen.sh— reproducible build (incl. the CFLAGS=-std=gnu17 fix)blockchain_block-rehearsal/payload.hex— winning payloadsend.py— TLS sender