Skip to content

TRI-NET: ternary compute node, verifiable receipts, mesh and settlement (review-only, already on main) - #294

Draft
gHashTag wants to merge 32 commits into
mainfrom
trinet
Draft

TRI-NET: ternary compute node, verifiable receipts, mesh and settlement (review-only, already on main)#294
gHashTag wants to merge 32 commits into
mainfrom
trinet

Conversation

@gHashTag

@gHashTag gHashTag commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Review-only. Already on main — please close, do not merge.

Every commit here is already merged into main, one at a time, over the
course of the 2026-08-01 session. This PR exists because that was the wrong way
to land 32 commits, and there was no single place to review the work.

The diff below is exactly this session's changes: GitHub takes it from the
merge base (891a711a3), which is the tree immediately before the first
commit.

Do not merge it. main has moved on — the other session working in this
repo has since edited three gf16_*_conformance_ax7203.py hosts on top of this
work, and merging would risk reverting their changes. The branch is a reviewing
surface, nothing more.


What landed

A ternary compute network: a node cell whose datapath is a 32-wide balanced
ternary dot product, a verifiable receipt, a mesh that judges and settles what
nodes claim, and an agent whose model arithmetic runs on it.

Measured on silicon (AX7203, XC7A200T)

trinet_mac32 — ternary dot product and CRC receipt 512/512 receipts verified
The same board answered by a second, independently written host Zig CLI over libc serial, 64/64
trinet_node_v2keyed receipt (SipHash-2-4) 256/256 with the key; every job rejected with a wrong key
gfternary MUL 16/16 exhaustive — but the datapath is FP32, not ternary
GF8 ADD, after the frame fix below 4096/4096 bit-exact
Delivered throughput 202.6 jobs/s, p50 4.90 ms
Logic cost 429 LC (v1), 1336 LC (v2), 0 DSP48 in both

The bug this session was worth finding

Commit 6f3001b17 (2026-07-17), titled "frame format bug fix", had inserted a
0x00 between the AA 55 magic and the payload across the conformance corpus.
The gf compute wrappers have no fmt field, so 32 hosts had been computing
gf_add(0, 0) for every input for two weeks
while every golden self-test
passed — none of them exercises the wire encoding.

Settled against the RTL rather than by reading Python
(formal/gf8_frame_regression_tb.v):

shipped frame   AA 55 00 3c 00 41 00 00  ->  adder received a=00 b=00
corrected       AA 55 3c 00 41 00 00     ->  adder received a=3c b=41

The recorded Tier-E passes predate that commit and stand; what was destroyed
was the corpus's ability to re-verify them. conformance/frame_alignment_check.py
now guards it in CI, and each CI run re-breaks a host on purpose and requires
the guard to fail on it
— a guard that has quietly stopped guarding looks
exactly like a clean run.

Two results that constrain what can be claimed

  • Device DNA is a dead end on this toolchain. DNA_PORT places, routes and
    answers — 8/8 reads, correct framing, 57 bits reported — and returns zero
    for all 57 bits
    . So node identity is asserted, not proven. The guard added
    in response was then seen working on hardware: v2 reports its fallback
    identity, where without it every board would have claimed 0x00000000.
  • Throughput is a UART property by ~5600x. Measured 202.6 jobs/s against a
    transport ceiling of 410 and a derived compute ceiling of 2.3M. No power
    figure exists, so no TOPS/W claim is possible.

Also corrected against itself

gfternary is not a ternary datapath — it expands each 2-bit code to an
FP32 constant, runs gf_mul_param(8,23), and re-quantises. Its 16/16 verifies
the format's decode/compute/quantise law and says nothing about ternary
arithmetic. trinet_mac32 is the cell that carries that claim.

TRI is a credit, not a token

src/trinet/ledger.zig issues a non-transferable internal work record, and
Ledger.init refuses to start on parameters where cheating has positive
expected value (p·s > r). The reasoning is in the module header and in
specs/trinet/settlement_law.t27.

Note for a reviewer: deploy/contracts/TrinityToken.sol already allocates 40%
of supply to node rewards, which is inconsistent with the design recorded here.


Verification

zig test src/trinet/agent.zig -lc                    # 42 tests
python3 conformance/frame_alignment_check.py         # 57 aligned, 0 mismatched
python3 conformance/trinet_mac32_conformance_ax7203.py --self-test
python3 conformance/trinet_node_v2_conformance_ax7203.py --self-test

Records: specs/trinet/ternary_hw_verification.t27,
specs/trinet/settlement_law.t27, docs/TRI_NET_ARCHITECTURE.md,
docs/TRI_NET_SESSION_REPORT_2026-08-01.md.

Tier-E proofs on #199.

🤖 Generated with Claude Code

gHashTag and others added 30 commits August 1, 2026 23:40
The TRI-NET node computes a 32-wide ternary dot product in pure LUT logic
(popcount of agreements minus popcount of disagreements, no multipliers and
no DSP) and returns a receipt whose CRC-32 tag binds the answer to the exact
job bytes, the nonce and the node identity.

Adds the RTL cell, an independent Python golden oracle with a receipt
verifier, an iverilog testbench that drives real UART frames, and CI that
gates synthesis on simulation agreement.

Simulation: 128/128 jobs bit-exact against the golden oracle.
Hardware: not yet measured.

Also wires up CI for the gfternary MUL wrapper, which had been written but
never synthesised.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 15cf207)
…e log

The guard grepped for DSP48 anywhere in the yosys output, which matches the
pass banner even when no DSP cells exist. Synthesis was clean at 429 LCs with
zero DSP48; only the check was wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit f8cb61b)
The wrapper parses a six-byte frame, AA 55 fmt a b trig, but the host emitted
a second fmt byte. Operands shifted by one, so the FPGA read a=0 for every job
and returned zero. That agreed with the golden oracle on the seven of sixteen
cases whose answer is zero, which is why the mismatch survived until the cell
was first run on silicon.

Hardware after the fix: 16/16 bit-exact on AX7203.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit e0990f3)
Six Zig modules and a .tri spec that turn the ternary node cell into a network:

  protocol  wire format and receipt verification, agreeing byte for byte with
            the RTL and the Python golden
  serial    physical node over the AX7203 UART at its synthesised baud
  net       TCP transport so a node on another developer's desk is reachable
            the same way as a local one
  node      fpga, remote and emulated backends behind one interface, including
            adversaries that skip the work, replay receipts or claim another
            node's identity
  ledger    TRI work credit, staking and slashing, with a policy that refuses
            parameters under which cheating has positive expected value
  mesh      dispatch, judgement and settlement kept as three separate steps
  model     ternary-weight inference where every matmul row is a mesh job
  agent     the IGLA CODER agent, with a proof bundle stating how much of its
            arithmetic ran on silicon

38 tests pass, including one showing a layer computes correctly while a node in
the mesh is lying, and that the liar earns nothing.

TRI credit here is a non-transferable internal work record, not a token.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit a801b2e)
…7 specs

Two .t27 specs in the project's own language, plus the architecture note they
summarise:

  ternary_hw_verification.t27  keeps RTL written, routed, and measured on the
                               board as three separate tiers; records the
                               gfternary 16/16 hardware result, the mac32
                               simulation, the host frame defect, and an
                               open_question for the hardware run that had not
                               finished
  settlement_law.t27           which layer answers which attack, why full audit
                               is affordable only for this work unit, and why
                               the TRI credit is not a token

Both carry scope_limits. The receipt explicitly does not establish where the
arithmetic ran, and that is recorded as an unresolved finding rather than
omitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 2ee1c93)
…al state

Records the status tier of every claim, the board and toolchain facts that cost
time to rediscover (a flash is 778 s not 78 s; openocd's redirected stdout is
block-buffered so an empty log is not a hang; CI workflows only register from
main), the frame-path bug class this project keeps producing, and the design
invariants that must not be collapsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit f60405b)
At threshold 4 the synthetic network's activations collapsed to the zero vector
by the third layer, so every task produced an identical decision with a zero
margin. Measured density per layer for a fixed input:

  threshold 0:  0.69 -> 0.91 -> 0.94 -> 0.91 -> 0.88   saturated, binary
  threshold 2:  0.69 -> 0.41 -> 0.28 -> 0.25 -> 0.31   sustained
  threshold 4:  0.69 -> 0.22 -> 0.03 -> 0.00 -> 0.00   dead

Both ends destroy the network for different reasons: too high and it answers
every input identically, too low and no trit is ever zero, which discards the
third state. The usable band is narrow because a 32-wide ternary dot product
has a standard deviation of about 3.4, so the constant is tied to the layer
width rather than universal.

Two tests pin both failure modes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit de7d037)
… ternary datapath

trinet_mac32 flashed and verified on the AX7203: 512/512 receipts, each pass
checking both the dot product against the golden oracle and the CRC-32 tag. A
second, independently written host (the Zig CLI over libc serial) reaches the
same board and agrees, 64/64.

Also corrects a claim this session nearly made. The gfternary compute cells
expand each 2-bit code into an FP32 constant, run gf_mul_param(8,23), and
re-quantise — so their 16/16 verifies the format's decode/compute/quantise law
and says nothing about ternary arithmetic. trinet_mac32 is the cell that
carries that claim, because its datapath is popcount and subtraction with no
floating-point core.

Records two further findings: three mutually incompatible trit encodings
coexist in this tree and will silently produce sign errors if wired together,
and deep ternary networks collapse to the zero vector above a width-dependent
activation threshold.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit fb85d36)
Commit 6f3001b, titled a frame format bug fix, inserted a 0x00 byte between
the AA 55 magic and the payload across the conformance corpus. That byte
belongs to wrappers whose parser has a fmt field. The gf compute wrappers in
fpga/vivado do not have one — they read six body bytes as op_a[7:0],
op_a[15:8], op_b[7:0], op_b[15:8], trigger — so the extra byte shifted every
operand one position and the core received op_a[7:0] = 0x00.

Settled against the hardware description rather than by reading Python:
formal/gf8_frame_regression_tb.v drives gf8_clean_ax7203 with both byte
sequences and reports what the adder latches.

  shipped host frame  AA 55 00 3c 00 41 00 00  ->  core received a=00 b=00
  corrected frame     AA 55 3c 00 41 00 00     ->  core received a=3c b=41

So every gf compute host has been computing 0+0 for each input since
2026-07-17, and the corpus cannot currently reproduce its own recorded
results. The recorded Tier-E passes predate that commit and are not
retroactively invalidated; the scripts that would re-check them are.

The three decode hosts were broken the same way in the other direction: they
append their own fmt byte on top of the inserted one, so code_lo received the
format id. gfternary is the one host where the inserted byte is genuinely the
fmt field, and it keeps it.

Every golden self-test passed throughout, before and after — none of them
exercises the wire encoding, which is the whole lesson.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 64c3459)
…ext-loop options

Records what is measured on silicon, the two corrections made against this
session's own earlier claims, nine ranked weak points, and three options with
stated cost, risk and falsification criteria.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 8912d8a)
… works

A static check that reads the request length each host builds and the length
its wrapper's frame FSM parses, and fails when they disagree. It needs no
board, so it runs on every push — which is the difference between catching a
frame regression in minutes and catching it in two weeks.

57 hosts aligned, 0 mismatched, 44 unchecked. Unchecked is reported honestly
rather than counted as passing: a guard that invents a verdict it cannot
substantiate teaches people to ignore it.

Two parser defects were found by testing the guard against a deliberately
broken host rather than by trusting it:

  - a trailing comment on the pkt line made the final term fail to match, so
    gf8_add fell silently into unchecked and the regression went undetected
  - the wide decoders latch through a counter, one statement covering sixteen
    bytes, so counting statements understated their frame and reported three
    false mismatches

CI re-breaks a host on purpose every run and requires the guard to fail on it,
because a guard that has quietly stopped guarding looks exactly like a clean
run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit abd81ba)
Thirteen skill files, including the two shared fragments nearly every other
skill inherits, hardcoded /Users/playra/trinity-w1 — a path that does not exist
here. Every command in them was silently unrunnable. Repointed to the actual
repository root.

fpga-synth carried a stale header and counts that had drifted from
fpga/CATALOG_MATRIX_83.md, so it now says plainly that the catalog is the
matrix of record. Added the environment facts a session otherwise rediscovers
the hard way — no local place-and-route, tri and zig-out absent, CI workflows
registering only from main, and a flash taking 778 s rather than the 78 s some
notes claim — and the frame-path failure mode with the guard that now catches
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4e16342)
The first run of this workflow failed with 503 and 404 across every mirror
setup-zig tries, which fails the job for a reason unrelated to the code. Fetch
from ziglang.org with a retry, and try both tarball naming conventions because
0.14 changed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4114adf)
The growth story rests on a node on someone else's machine being reachable the
same way as a local one, so the test opens an actual listener, serves the same
24/15-byte framing the FPGA speaks, and checks the credit lands. 25/25 jobs
credited to the remote owner.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit f5a9da3)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 1e52588)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 59ce240)
Reads the 7-series factory device DNA through DNA_PORT and streams it over the
same UART the compute nodes use. yosys already answers the first half: the
primitive survives synth_xilinx. This cell answers the second half, whether
nextpnr-xilinx will place and route it.

The third half is answered in the header and does not need silicon: device DNA
is an identifier, not a key. It is readable over JTAG and by any bitstream on
the part, so once a value has been read, software can claim it forever. Binding
a receipt to the DNA proves a bitstream asserted an identity — not that the
arithmetic happened on that chip. Unforgeability needs a secret that never
leaves the device, which here means eFUSE or BBRAM plus an encrypted bitstream.

Simulation passes over two rounds against a DNA_PORT stub, including that the
value does not change between reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 42c1ac4)
Generated by substitution and left a doubled list marker, which GitHub rejects
before running anything. Nothing was learned about DNA_PORT from that failure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 4f268c0)
…er conversation needs

trinet bench times real jobs against the board and prints the ratio that
matters: what the transport allows against what the cell could do. A network
whose nodes spend every job waiting on a serial line is a serial line with a
compute network attached, and the way to find that out is to measure both ends
and print the gap rather than quote the good one.

The brief states only measured numbers, names what is not measured so it cannot
be inferred, and is explicit that the artifact's near-term value is a
reproducible open-toolchain ternary baseline and a verification discipline —
not throughput. It ends by asking for the answer that would be least welcome.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit f3e6737)
SipHash-2-4 over the 26-byte receipt preimage, one round per clock, 22 cycles
per tag. A CRC-32 tag detects corruption but resists no forgery — the function
is keyless, so anyone can produce a valid tag for any job. A keyed PRF makes a
tag something only a key holder can produce.

Chosen for being add, xor and rotate with no multiplier, which keeps the
zero-multiplier discipline the rest of the node holds to, and for having
published vectors so three implementations can be held to one law. The RTL
reproduces Zig's std.hash.SipHash64(2,4) exactly across two keys and a repeat.

The header is explicit about what this does not buy: it moves the trust
boundary from anyone to anyone who can read the key out of the bitstream. Since
operators hold their own bitstream it does not stop an operator forging their
own receipts — it stops everyone else forging on their behalf, and with
per-node keys it stops one operator forging for another. Unforgeability by the
operator needs a key that never leaves the device, meaning eFUSE or BBRAM plus
an encrypted bitstream. This is the substrate that path needs, not a substitute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 12c7da5)
Verilog, Zig and Python all reproduce SipHash-2-4, checked against the
published vectors for the empty, 3-byte and 26-byte messages. The 26-byte one
is the receipt preimage length, so the vector the RTL testbench asserts is the
same value the hosts compute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit e025c74)
Same ternary datapath, receipt tag upgraded from CRC-32 to SipHash-2-4 under a
key that exists only inside the bitstream, and the node id taken from the
factory device DNA rather than chosen at synthesis.

6/6 keyed receipts bit-exact in simulation against the Python golden, with the
id arriving from the DNA_PORT stub rather than the fallback parameter — so the
DNA is reaching the receipt, not just the response.

Both limits are in the header rather than left to be discovered. The key lives
in the bitstream and the operator holds the bitstream, so this stops third
parties forging on an operator's behalf and stops one operator forging for
another; it does not stop an operator forging their own. The DNA is readable
over JTAG, so it is an identifier, not a secret: it stops two boards shipping
with the same id, not software claiming an id it has seen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 0eb3197)
…the receipts

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 28955bd)
8'(MSG_BYTES) compiled under iverilog -g2012 and was rejected by yosys outside
SystemVerilog mode, so the core simulated correctly and failed synthesis. A
sized localparam does the same job in both.

Adds the DNA probe host, which treats a value that changes between reads as a
failure — an identifier that is not stable is not an identifier.

Local yosys now elaborates the v2 node: 1343 LCs, one DNA_PORT, zero DSP48.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 6713573)
Measured on the AX7203 through openXC7: DNA_PORT places, routes, and completes
its read sequence — 8/8 reads, correct framing, 57 bits reported — and DOUT is
zero for all 57 of them. All bits reading zero points at the primitive not
being configured by the bitstream rather than at a wrong shift sequence, which
would be expected to produce offset bits rather than a uniform zero. Separating
those two fully needs a vendor-toolchain build on the same board, which has not
been run.

Without this guard the v2 node would have derived node id 0x00000000 on every
board on this flow — one measurement turning into a network-wide identity
collision. It now falls back to its parameter identity when the DNA is
unavailable or zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 892f69f)
…throughput

Three results from working options A and B, each at the tier its evidence
supports:

  DNA_PORT routes under openXC7 and returns an all-zero DNA on silicon —
  recorded as PARTIAL, with the inference stated and the comparison that would
  settle it named as not run

  the keyed receipt tag reproduces the published SipHash vectors in three
  implementations, with the boundary it does not move written down

  202.6 jobs/s measured against a derived compute ceiling 5606 times higher —
  the ratio that governs every performance claim about this node

Adds an open_question for TOPS/W noting that no power figure exists and that
publishing an efficiency comparison before one would be fabrication.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit e1daf5a)
…futed itself

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 47b2037)
… reorder the wave

The next wave was ordered on the assumption that DNA_PORT was untested and
throughput unmeasured. Both are now measured, and both came back constraining,
so the ordering changes: demand first, then escaping the UART, and an explicit
instruction not to re-run the DNA probe expecting a different answer.

Adds the table of exactly what each receipt tag resists, so the reach of the
keyed version is not overstated later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 9b81a45)
Section 5 said whether openXC7 could instantiate DNA_PORT was untested. It has
been tested: it routes, it answers, and it returns zero for all 57 bits. Also
records that the keyed tag partly closes the unforgeability gap and states
exactly how far.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 855f41d)
Verifying that the tag matches is not enough on its own — a tag computed with
the wrong key must also fail, or the key is decorative and the upgrade bought
nothing. The self-test asserts both directions on every vector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 2ca422e)
gHashTag and others added 2 commits August 1, 2026 23:40
It imported the shared golden by bare module name, so it only worked when the
working directory was conformance/. Every other host in the corpus is invoked
from the root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 08b66b5)
…seen working

trinet_node_v2 flashed and measured: 256/256 keyed receipts verified with the
key, every job rejected without it. The second run is the load-bearing one — a
tag that verified regardless of the key would mean the key never reached the
receipt.

The node reports its fallback identity rather than a DNA-derived one, which is
the zero-DNA guard engaging on hardware exactly as intended. Without it every
board built this way would have reported node id 0x00000000 and a network-wide
identity collision would have shipped looking like a clean run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
(cherry picked from commit 5a5c334)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant