T27.AI

Blog

The board's receipts were never checked. Now all 403,200 are.

2026-09-27 · 11 min read

[measured on FPGA: all 42 trained matrices, every receipt verified; the activations were test vectors, not a forward pass] An AX7203 computed 28,416 of 28,416 rows of the trained tern_tc model's weight matrices bit-exact, while its harness reported 284,160 authenticated receipts without comparing a single tag. The fixed harness fails on seven kinds of misbehaving cell. On the board it then verified all 403,200 receipts across all 42 ternary matrices, with 33,792 of 33,792 rows bit-exact, plus 51,840 for w_down with int8 activations. Its first long runs stopped when a UART link dropped bytes with 64 jobs in flight. A test registered before it ran put the loss threshold at the USB bridge's 512-byte receive buffer, and at 24 in flight none were lost.

#IGLA#FPGA#Ternary#Verification#TRINET#Correction

IGLA's board-sized model, tern_tc, has 9.08M parameters. 6,451,200 of them are ternary block weights in 42 matrices across six layers, trained on 2.0B tokens of code to 0.7613 validation bits per byte. The board is an ALINX AX7203 (XC7A200T) running the TRI-NET node cell, built with the open openXC7 flow: a 32-trit ternary dot product with no multiplier and no DSP48. It answers each job with the result, the job's nonce, a node id and a SipHash-2-4 tag over all of them. The host splits every 320-wide row into ten 32-trit jobs and adds up the answers.

What the board did

Run (AX7203, 1,144,744 baud)JobsRows bit-exactReceipt tags compared?Time
Random 320x320 ternary matvec3,200320 / 320yes0.67 s
Layer 0 wq, trained weights12,8001,280 / 1,280no2.7 s
wq, wo, gate, up in all six layers284,16028,416 / 28,416no62.4 s

The activations were ternary vectors, not the model's int8 activations. So this shows the trained weight matrices computed exactly. It is not a forward pass of the model.

The receipts in the last two rows were never compared

The layer harness built each receipt's preimage and then dropped it, and the key it loaded was never used. It counted status byte 0x01 as authentication. It never checked that the nonce came back, and it compared only row sums, so two wrong chunks could cancel. We gave it a software model of the cell that signs every answer with a key the host does not hold. It printed 'receipts authenticated 160/160' and PASS.

What stands: every row value in the table matched the CPU oracle, and the random-matrix run did compare its tags. What is withdrawn: '284,160 receipts authenticated under node0's key', because those receipts were never checked. The fixed harness has since measured all 42 matrices on the board, those 24 included, with every receipt verified (below).

The fixed harness, and seven ways to make it fail

A response now counts only if all five of these hold. Its status is 0x01. Its nonce was issued by this run and answered once. Its node id matches the first answer. Its SipHash tag recomputes under the key. Its y equals that chunk's dot product. A row passes only if every chunk passed and their sum equals the row dot computed directly from the model's int8 weights, not from the packed wire bytes. Each check is shown able to fail:

What the cell does wrongHarness verdict
signs with another keyrejected: tag
returns a wrong answer and signs it validlyrejected: lie
flips one tag bitrejected: tag
answers with a nonce the run never issuedrejected: fabricated
answers as another node idrejected: node
drops a responserejected: short read, run stops
was never keyed (status 0x04)no credit, though y is right

After the board run below, an eighth control was added, for the link rather than the cell. It is a stream that loses 16 bytes in the middle, as the board's link did. The harness credits the answers before the hole, rejects the damaged one and stops.

The same bytes through the RTL

The cell's own source, trinet_node_core.v and trinet_siphash24.v, unchanged, was simulated in Icarus Verilog at UART bit level. The key was installed over the wire exactly as on the board. The weights are random ternary values in tern_tc's exact shapes, because the trained model file is not in this environment.

RTL runJobsRows bit-exactReceipts verified
Layer 0, all seven matrices, w_down as 27 chunks67,2005,632 / 5,63267,200 / 67,200
Layer 5 w_down, int8 activations (6 digit planes)51,840320 / 32051,840 / 51,840
Layer 0 wk, node never keyed6400 / 640 / 640 (status 0x04)
Layer 0 wk, checked under a different key1,2800 / 1280 / 1,280

The fixed harness on the board

On 2026-09-27 the fixed harness ran on the AX7203 with the trained model file. The host was an M1 Pro, talking to the board's CP2102N UART through a USB hub. The x-vectors are random test vectors from a fixed seed, not the model's real activations.

Board run (1,144,744 baud)Jobs in flightReceipts verifiedRows bit-exactResult
Random 320x320 ternary matvec643,200 / 3,200320 / 320PASS, 0.79 s
All 42 matrices, ternary x6418,984 of 403,200, then the link lost 16 bytes1,898 / 33,792FAIL, stopped
Layer 5 w_down, int8 x649,886 of 51,840, then the link lost 4 bytes61 / 320FAIL, stopped
Layer 5 w_down, int8 x851,840 / 51,840320 / 320PASS, 11.9 s
All 42 matrices, ternary x24403,200 / 403,20033,792 / 33,792PASS, 85.5 s
All 42 matrices, ternary x (control)646,679 of 403,200, then the link lost 59 bytes667 / 33,792FAIL, stopped
All 42 matrices, ternary x (registered)26403,200 / 403,20033,792 / 33,792PASS, 85.3 s
All 42 matrices, ternary x (registered)30149,986 of 403,200, then the link lost 7 bytes12,822 / 33,792FAIL, stopped

The window-24 row is the result this post was waiting for. All 42 ternary matrices of the trained model, all 6,451,200 weights, ran on the existing bitstream. The host checked every one of the 403,200 receipts: status, nonce, node id, the SipHash tag recomputed under the key, and y. All 33,792 rows equal the int8-weight oracle. The run took 85.5 s, 4,716 answers per second.

The w_down run at 8 in flight is the first board run of trained weights with int8 activations and every receipt checked: 51,840 tags and 320 rows. It covers one matrix of 42, with one random activation vector.

The four failures are not wrong answers. All 185,535 answers that arrived whole before a hole had the right y and a verifying tag. The one answer each hole tore through failed a check and was refused. The link is what failed. The answer stream lost 16, 4, 59 and 7 bytes, with intact bytes on both sides. The cell cannot produce that pattern, because it sends every answer whole from one buffer. The harness did its job: it stopped at the first unframed read and credited nothing after it.

The window matters; the control shows it. The same full run, on the same harness, setup and session, went clean at 24 jobs in flight and lost bytes after 6,744 jobs at 64. So the pass comes from the window, not from the new harness's timing and hex output. At 64, three long runs slipped three times.

The first explanation does not survive the control. The idea was that answers pile up while the harness process looks away. The fixed harness measures those pauses. In the failing control at 64 the longest was 4.2 ms, about 1,700 answers before the hole. Passing runs survived pauses of 22.4 ms at 24 in flight and 25.8 ms at 26.

What does fit is the USB bridge. The board's UART goes to the host through a CP2102N, whose datasheet gives a 512-byte receive buffer and asks for handshaking above 1 Mbaud to avoid receiver overrun. The node runs at 1,144,744 baud and has no handshake lines, so nothing can hold it off. With W jobs in flight, at most 19 x W answer bytes are on their way. The next test was registered before it ran: a pass at 26 in flight (494 bytes) and a slip at 30 (570 bytes). Both happened. Window 26 carried all 403,200 jobs, and window 30 lost 7 bytes after 150,017 jobs. That puts the loss threshold between 494 and 570 bytes, around the 512-byte buffer.

One side prediction missed. The slip at 30 came after 150,017 jobs, not within the first 20,000 as at 64, so the stalls that fill the buffer are rarer than a single fixed gap would explain. What stalls the bridge's USB transfers is not measured, and the hub is not excluded. The rule the harness follows now is to keep 19 x W under 512 bytes, which is why the default is 24 (456 bytes). It costs no measurable throughput: 4,716 answers/s, against about 4,660 to 4,750 at 64 before the slips.

Two items booked as new hardware need none

w_down was left out because its input is 864 wide and the plan called for a wider cell. It does not need one: 864 = 27 x 32, so it is 27 jobs per row on the same cell. wk and wv have 320-wide inputs and were simply skipped. Together that is all 42 ternary matrices and all 6,451,200 weights on the existing bitstream.

int8 activations were Stage B.3, which planned new RTL. Every integer from -364 to 364 is a sum of six balanced-ternary digits: q = sum of 3^k d_k with each d_k in {-1, 0, +1}. So w.q = sum of 3^k (w.d_k), and each w.d_k is an ordinary ternary job. Six jobs per chunk replace a new datapath. The cell stays ternary, and the host applies the powers of three in the open.

What this is not

Next

  1. Lift the link limit: add RTS/CTS flow control to the node, as the CP2102N datasheet asks above 1 Mbaud. Until then, keep answers in flight under 512 bytes, as the harness now does.
  2. Real activations: dump the int8 inputs tc_infer computes for a real prompt, and run a whole layer with them.
  3. Leave the UART: move to Ethernet or a USB FIFO, and measure it on the board.
  4. Make receipts checkable by anyone: publish a Merkle root of each run, and add random re-execution or Freivalds checks. Both are cheap for ternary matvecs.
  5. Measure power on a bench supply. Without it there is no energy comparison to publish.

What this does not settle

Receipts

Work with me

Want this kind of check on your own design?

I audit RTL and build independent, bit-exact models, then take the result through synthesis and, when useful, onto an Artix-7 board. The first conformance module is free.