Field report/ 17 Aug – 4 Sep 2026/ Jane Street ASIC puzzle

The Star Battle Chip

Nineteen days from “what does reverse-engineering an ASIC even mean?” to a mask-level layout decompiled into gates, simulated, SAT-solved, and proved complete — and the discovery that the silicon is a puzzle validator with a Latin motto written on it in Morse code.

Die200×352.72 µm
Placements9,875
Logic cells728
Flip-flops92
Input121bits
Answer15bytes

01The brief

One binary file, no names, no netlist, no source.

Jane Street’s 2026 puzzle ships a single artefact: puzzle.gds, a GDSII stream — the format a foundry receives. It is not source code and not a schematic. It is photomask geometry: polygons on numbered layers, where layer 66 is polysilicon and layer 68 is the first metal, plus thousands of placements of named library cells. Everything above that level — module boundaries, signal names, the design’s intent — was compiled away.

The stated goal is to drive a success output high. The real goal, buried in the README, is that “you’ll need to simulate it to get your final answer” — the chip emits a string on O[7:0], and that string is the submission. success is a checkpoint, not the finish.

A warmup/ directory carries the same design at every compilation stage — RTL, netlist, placed netlist, GDS. That is the Rosetta Stone: any tool built to run the pipeline backwards can be checked against ground truth before it is ever pointed at the real thing.

Constraint accepted at the outset

The blog permits AI for tooling and for the warm-up, and forbids feeding it the puzzle files or having it write the submission. So the rule for the whole campaign was: build instruments, never read the evidence. Every command that touched puzzle.gds was run by hand; the answer arrived as tool output, not as a model’s claim.

02Campaign

Eight working sessions. The first three build understanding, the next three build instruments, the last two use them.

17 AugDay 1

Framing, and Verilog from zero

Opening question — “does reverse-engineering mean recovering the original Verilog?” — is answered no: the deliverable is behavioural equivalence, not textual recovery. Then warmup/00_source.v line by line, followed by nine questions that go all the way down: what is fanout, what is a net, what is a flip-flop, why does combinational logic need a clock at all.

Established: combinational vs sequential, the DFF, the sky130_fd_sc_hd__dfrtp_2 naming scheme, and why 6 pins can carry 16 bits of state.
27 AugDay 11

Silicon literacy

A two-column annotated reference is published so question and answer sit side by side instead of scrolling apart. Then the RTL→GDS pipeline — Yosys, ABC, place-and-route, stream-out — and which stages are reversible. GDS is explained as “SVG, but the layers are physical materials”, which is where the analogy earns its keep: an SVG layer is cosmetic, a GDS layer is a fabrication step.

Two standing instructions recorded: no spoilers from puzzle.gds, and no package installs without asking.
28 AugDay 12

Fighting KLayout, then escaping it

The layout viewer is opened, layer numbers (64/5, 66/20) decoded, and its macro IDE turns out to have a genuine defect: the Run button stays bound to the wrong file no matter what is selected. Two workarounds are tried before the right move — abandon the GUI, run klayout -b -r script.py, then abandon that too for the klayout PyPI module and a normal venv with a debugger.

tools/survey.py — layer table, cell inventory, per-cell geometry dump. Scripts now live in the repo and in git.
30 AugDay 14

Geometry back to a netlist

KLayout’s LayoutToNetlist is pointed at the warm-up GDS: conducting layers declared, via layers bridged, each standard cell recovered as a subcircuit with pin names read from the text labels inside it. The output is compared to the reference netlist not by name — the extracted names are meaningless — but by net signature: each net reduced to the multiset of celltype.pin it touches.

Signature multisets identical. 230/230 cells, 84/84 nets, 18/18 cell types. The extraction is isomorphic to ground truth.
31 AugDay 15

A graph, a simulator, and a solver

Three modules in one day. A cell library carrying Boolean expression trees; a netlist parser that infers port directions from usage and exposes fan-in cones; a cycle-based simulator. Then the conceptual jump — a SAT solver has no notion of time, so time is turned into space. The circuit is copied once per cycle and frame k’s flop outputs are wired to frame k+1’s inputs, which turns a cyclic circuit into an acyclic formula. Explained with a visual artifact before any code was written.

Warm-up solved by z3 in 0.01 s against 109 s for exhaustive simulation — and cross-checked by replaying the solver’s answer through the independent simulator.
1 SepDay 16

First contact with the real die

The survey runs on puzzle.gds: 9,875 placements, 80 cell types, and two names that belong to no library — INTERNAL_3 and INTERNAL_7. Fifteen cell types have no definition. A review pass strips 80 lines of speculative flags from the toolchain, with the standing instruction not to cut anything the puzzle might need.

Library gate added: --check-lib refuses to proceed while any placed cell type lacks a definition, because a wrong cell function corrupts every result silently.
2 SepDay 17

Debugging, then the answer

The hardest day. Hand-written cell definitions are thrown out and re-derived from the PDK’s own Liberty data. An undriven wire is chased for hours. Eight flip-flops turn out to be the wrong type to reset to zero. Escaped Verilog identifiers silently collapse all eight output bits into one port — twice, once in the reader and once in the writer. Then, with the blockers cleared: 122 frames, one unique input, fifteen bytes out.

(* TWO STARS *) — and, hidden on a layer below the die outline, a strip of Morse code.
4 SepDay 19

Proving there is nothing else

Deadline day, spent on completeness rather than the answer. Jane Street’s own reference waveform is parsed and replayed through the extracted netlist. The set of messages the chip can emit is enumerated by SAT until UNSAT. The 121-bit input is recognised as an 11×11 grid, the region map is recovered behaviourally, and the puzzle is re-solved from scratch to confirm it. A forked-file sweep looks for further eggs and finds none.

312/312 cycles match the reference trace. Five messages, proved exhaustive. 81 of 92 flip-flops identified by role.

03Reduction

Each stage discards something provably inert. The whole point is that every arrow is checkable.

9,875
placements
What is physically in the top cell of the GDS.
728
logic cells
After dissolving 8,200+ via cells and 880 fillers, taps and decaps — all verified to carry no signal.
92
flip-flops
The entire state of the machine. 695 cells and 706 nets survive cleaning.
121
input bits
One serial stream on I, enable high throughout, verdict at frame 122.
15
bytes out
Frames 122–136, then the output mask closes for good.

The success cone is 484 of 728 cells, and it sits entirely inside the O[7:0] cone — nothing feeds the verdict without also feeding the output. Thirteen flip-flops belong to the output generator alone, which is precisely the region the puzzle README says can be ignored during reverse-engineering.

04The proof chain

The reason to trust the answer is not that a solver returned SAT. It is that five independent methods were made to agree.

CheckMethodResult
Extraction is faithful Net-signature isomorphism against the warm-up’s reference netlist — name-independent 230/230 cells
84/84 nets
Cell semantics are real Library parsed from the PDK’s Liberty JSON, not hand-written; exhaustive 65,536-pair self-test reproduces the warm-up RTL exactly 0 mismatches
The netlist matches the original chip Jane Street’s own example_inputs.vcd replayed through the extracted netlist, comparing O and success every cycle 312/312 cycles
The solver and simulator agree Every z3 model replayed through the independent Python simulator; disagreement aborts rather than reporting PASS
The input is unique Blocking-clause enumeration over generalised cubes, run to exhaustion 1 solution
UNSAT after
The length is minimal 121 cycles UNSAT, 122 SAT — pinning the protocol at exactly 121 clocked bits plus a settle frame 122 frames
Nothing else can be emitted Message vectors blocked whole and re-solved until UNSAT, after first proving by SAT that no output is possible outside frames 122–136 5 messages
27 byte values
The recovered puzzle is the right one Star Battle rebuilt from the extracted region map and solved from scratch in z3, independently of the netlist 1 solution,
identical
The check that mattered most

Replaying the reference VCD initially showed 20 mismatches on O, zero on success. That pattern — one signal wrong, the other right — is the signature of a sampling offset rather than a logic error. Shifting the comparison by one cycle gave 0/312. The VCD logs O at the clock edge; the simulator returns the value computed during the cycle. Same trajectory, different label.

05What the chip turned out to be

121 = 11 × 11. The input was never a bit stream. It was a board.

Read the winning input row-major into an 11×11 grid and the structure is immediate: exactly two stars in every row, exactly two in every column, and no two stars touching — not even diagonally. The chip is a Star Battle validator, and the messages it emits are the rules of the game.

  • 22 stars — matching the winning stream’s popcount exactly.
  • Every run of 1s has length 1. The two failed attempts in the reference VCD have runs up to 4 and 38 stars each — nowhere near legal.
  • Rows, columns and no-touching alone admit 500+ grids, but the chip accepts exactly one. So it also encodes the region map — the irregular blobs that must each hold two stars.
  • The regions were recovered behaviourally, not from geometry: feed 121 grids each holding a single star, and see which counter flip-flop moves. Eleven flops came back with square-sets that tile the board.
  • Sizes: 4, 5, 6, 7, 8, 8, 9, 11, 14, 21, 28. Rebuilt as a fresh Star Battle and solved in z3, it has one solution — the chip’s.

The architecture then explains itself. 121 squares but only 92 flip-flops means the grid cannot be stored; it must be scored incrementally as bits arrive:

CountRoleEvidence
12Shift register — the adjacency window Only stages q0, q9, q10, q11 tap out to logic: squares i−1, i−10, i−11, i−12. That is left, up-right, up, up-left — which is why it is 12 bits and not 11.
2211 column counters, 2 bits each Saturating: 0, 1, 2, 3 — and a fourth star still reads 3.
2211 region counters, 2 bits each Recovered by the single-star sweep; an exact-cover search found only two ways to tile 121 squares, the columns and these.
8Position counter (4 row + 4 column)Addresses the readout.
8Output character generatorOne private flop per output bit — the characters are computed, not stored in a ROM.
4Readout position counter16 positions, matching the 15-byte burst.
1Row counterClears at each row boundary — which is why it never showed up in end-of-run scans.
3Total star count, done flag, output enable
11UnidentifiedLeft unresolved at the deadline.
TRY AGAINany other grid — 200/200 random inputs
EMPTY SKYall zeros — no stars placed
BIG BANGall ones — every square a star
TWO NOT TOUCHthe adjacency rule, stated by the chip
(* TWO STARS *)success — two per row, two per column

Five, and SAT-proved to be all of them. The answer is also a joke in the house language: an OCaml comment whose delimiters contain exactly two asterisks — a comment that counts its own stars.

06The die art

It was visible in the very first command run against the file, and walked past for two weeks.

The survey printed bbox (0,-52.72; 200,300). A standard-cell array starts at y = 0 by construction — rows stack upward from the origin. Negative y means something was deliberately placed outside the design. That 52.72 µm strip is the whole find, and no rendering was needed to spot it.

What is down there: 36 marks on layer 200/0, in one row, in two widths. Quantised against a 1.38 µm unit, the widths are {1, 3} and the gaps are {1, 3, 7} — with nothing left over. That is the ITU Morse timing specification, not a coincidence.

P E R A R E N A M A D A S T R A
layer 200/0 · y = −52.72 µm · 36 marks · unit 1.38 µm outside the prBoundary (0,0;200,300)

PER ARENAM AD ASTRA

“Through the sand, to the stars.” — per aspera ad astra with aspera (hardships) swapped for arenam (sand). Silicon comes from sand.

Which makes (* TWO STARS *) literal as well as self-referential: the die’s motto is about reaching the stars, and the payload it hands back is two of them. The strip sits deliberately outside the official die outline — the prBoundary is (0,0;200,300) and does not contain it. It is also absent from the supplied layout.png render, so the only way to it was the file itself.

07Ledger of failures

Kept because the interesting ones share a property: the regression suite stayed green through most of them. A test that passes on the warm-up says nothing about a construct the warm-up does not contain.

Silent — would have produced a confident wrong answer Caught by a check Corrected under challenge Dead end
Silent

Cell library written from the naming scheme, not the datasheet. Fifteen missing cell types were filled in by inferring functions from names like o2bb2a.

A wrong Boolean function corrupts every simulation and every SAT result with nothing to warn you. Fixed by discarding the hand-written library entirely and parsing 70 cells from the PDK’s Liberty JSON — including deriving reset/set polarity from the ff() group, and raising rather than guessing on an active-high clear.

Silent

Every flip-flop pinned to 0 at reset. The design has 84 dfrtp, 4 dfstp and 4 dfxtp — a dfstp leaves reset at 1, and a dfxtp has no async control at all.

Worse than a plain bug: the solver and the simulator shared the assumption, so --verify would have agreed with itself while both were wrong. Initial state is now derived per-cell from Liberty.

Silent

Bus indices eaten from escaped identifiers. The port regex stripped [...] assuming a bus width, so \O[0]\O[7] all collapsed to one port named O.

Correct for input [7:0] data, wrong for an escaped identifier where the brackets are the name. Present twice — once in the reader, and again in the writer, which emitted the illegal output O[0]; so the fixed reader hit the same bug on the next pass.

Silent

Hierarchy counted one level deep. The inventory walked only the top cell, so the 21 INTERNAL_3 and 15 INTERNAL_7 containers hid their contents.

The library gate could have passed while an undefined cell type sat inside a container. Found by the user asking whether those cells were in the output at all.

Silent

Shift-chain detection recognised only mux2 hold paths. On the puzzle, 51 of 92 flip-flops build their hold path from ordinary gates — nand2b, o21a, a21o.

The warm-up used mux2 exclusively, so this could never have shown up there. The first fix then over-detected — a counter bit also reads its own Q back — and needed a truth-table query to distinguish pass-through from arithmetic. Only possible because the library carries real functions.

Silent

--verify drove rst_n randomly, re-asserting reset on roughly half of all cycles.

The design barely left its reset state, so the equivalence check proved almost nothing while reporting PASS. Related: a proposed --free-init mode was outright unsound — it let the solver invent a starting state no reset can produce.

Caught

A missing pin connection silently skipped 8 of 63 cells.

Fault injection showed one deleted connection cascading through the design with no diagnostic — and clean.py --verify comparing two identically-broken simulations and printing PASS. A warning now fires inside step(), so every caller inherits it.

Caught

“ALL GREEN” printed over three real failures. set -e does not fire inside a pipeline.

Three regression steps had produced no output at all. Same session also broke survey.py by re-opening a docstring in a replacement — caught only because a syntax check ran.

Corrected

“Fixed” a floating net that was never fixed. An undriven wire, n1447, was chased across a whole session. Three other bugs were found and fixed along the way, the count went from 15 problems to 1, and that got reported in a way that read as resolution.

Called out directly — “Are you sure it’s all correct now?” — and retracted. It was then settled properly and empirically: 3,000 random cycles with the net forced to 0 and to 1 produce identical outputs, so it cannot affect anything. Its forward cone contains zero sequential cells.

Corrected

A false negative that hid the one place it did matter. The check “is this net in any output’s cone?” compared a net name against a set of cell names — it could never match.

The net reaches O[1] and O[4]. That is exactly why a fifth message came out two different ways in two runs. The enumeration now leaves the net free per-frame and reports the indeterminacy rather than hiding it.

Corrected

“J S C” — a company acronym read out of three grid regions.

One region is unmistakably a blocky S. The second was pattern-matched, the third forced — a shape with a full bottom bar and a stray square called a J — and then the reading order was chosen to produce the acronym. Fitting the data to the conclusion. Retracted under questioning: one letter spells nothing, and an S is the most likely shape to appear by chance.

Dead end

Six identically-named scopes in the reference VCD, pushed four times.

Viewers merge same-named scopes, so the file renders exactly like the conventional form — there is no channel there to hide anything in. Settled by generating two rewritten VCDs and loading all three side by side. It was a generator quirk.

Dead end

Hunting the “two stars” as drawn geometry.

A vertex histogram showed every shape on every layer was a 4-gon, killing star-shaped art. Metals and placement were rendered — power straps and functional clumps. All 893 text objects turned out to be standard-cell pin labels. The actual find needed none of it, only the bounding box.

Dead end

KLayout’s macro IDE ran the wrong file, permanently.

Tree selection, saving, reopening, F5 — none of it moved the run binding. Never diagnosed. Escaped via a one-line loader stub, then abandoned for batch mode, then for the PyPI module. The right call, two workarounds late.

Six course corrections came from the user, not from a test

“Don’t give away any answers — I need to work it out myself.” · “Don’t perform any installations yourself.” · “Why not patch directly, why write a Py script to do this?” · “You can read the file, why are you doing these tricks?” · “Do you really need a new script instead of adding it to survey.py?” · “Why are you holding rst_n=1?”

The last one had no good answer — it was a convenience carried over from the warm-up and never re-examined. Investigating it exposed the unsound --free-init mode.

08The instrument

Thirteen files, 4,033 lines of Python. Everything runs from the command line.

ToolLinesDoes
survey.py404Layer table, recursive cell inventory, per-cell geometry dump, library gate (--check-lib), inert-cell detection by height and power pins
extract.py206GDS → Verilog netlist via LayoutToNetlist, via flattening, filler stripping
celllib.py195Cell semantics parsed from the PDK’s Liberty JSON — no hand-written functions
crosscheck.py387Liberty function parser, shared with the cell library
netlist.py487Parser and graph: direction inference, driver/sink invariants, backward and forward cones
clean.py391Graph rewrites — directions, clock-tree collapse, structural net naming — with equivalence verification
sim.py627Cycle-based simulator: topological evaluation, per-cell reset semantics, register recovery, CSV output
solve.py558z3 bounded model checking: unrolling, cube generalisation, all-solutions enumeration, VCD emission
sim_special.py249Proves the output-message set complete by enumerating to UNSAT
vcd.py125VCD → CSV, for replaying the reference trace against the extraction
sweep.py158Batch stimulus runs with randomised tails
extract_cells.py167PDK cell data extraction
compare_netlists.py79Name-independent structural isomorphism check
The winning stimulus — 121 bits, enable and rst_n high throughout, success at frame 122:

0000000101010000100000000000010101010000000000001010000001000001000000100000101000010000000100000010000010010001010000000

09Left open

Eleven of 92 flip-flops were never assigned a role. The census covers the shift register, both counter banks, the position and readout counters, the character generator and the flags — the remainder was not chased before the deadline.

One net has no driver. Its geometry is two cell input pins joined by a single top-level metal wire with vias only at the ends; no third cell’s geometry touches it, every conducting layer in that region is registered, and nothing lies within 400 nm. It is either a genuine floating net in the design or an extraction gap that could not be measured. Either way it was proved inert for success, and its only reach — two bits of one non-winning message — is reported as indeterminate rather than papered over.

Two of the three “letterform” regions were a false reading, withdrawn. Whether the remaining S is intentional is unknown and probably unknowable.

The final completeness query — the message alphabet with enable and rst_n both left free, roughly 142,000 variables over 200 frames — was still running when submissions closed. The held-enable version had already returned UNSAT.