Repository navigation
fix(trinet): a coordinator restart no longer pays the same (node, nonce) again - #907
Conversation
…ce) again ledger.zig kept the paid (node, nonce) set in memory and mesh.zig started next_nonce at 1 on every start, so a restarted coordinator credited the same (node, nonce) twice (two runs, nonce 1: 2 mTRI) and a receipt saved from an earlier run could match a reissued nonce. journal.zig is the coordinator's durable record, per gHashTag/t27 specs/trinet/node-work-credit.t27: - a nonce mark is written before any nonce past it is issued, and a restart resumes at the last mark, so no nonce is issued twice; - a credit line is fsynced before the credit counts, and the paid set is rebuilt from the journal on start; - an unreadable journal or a malformed line refuses to open; only a torn last line is skipped; - each credit says its level: self_reported when the coordinator's operator is the node's owner or unknown, verified otherwise. A nonce this run did not issue, or a credit the journal could not take, is not_recorded: not credited and not slashed. TRINET_JOURNAL, TRINET_OWNER and TRINET_OPERATOR wire it into fleet, demo and agent; without TRINET_JOURNAL nothing changes. The trinet-tests job now also runs when src/trinet changes. zig test src/trinet/agent.zig -lc: 67/67 (11 new); 9/9 code mutants caught by a failing test. Closes #906
Summary by t27-beesThis PR introduces a durable journal ( Worth a look
Read: 5 files, +897 -7, the whole diff. Head |
X Brain Health CheckScore: 100.0/100 X Brain is above merge threshold |
|
CI at head 5094fe4:
I am not merging while those two are red. Fixing them needs a decision on the research notes, which is not part of this PR. Dmitrii Fedorov (@dmitrii-f-t27) |
🧪 Brain Stress Test ResultsScore: not measured (no stress test prints a Score: line) X Stress test passed |
5094fe4 to
e9c2486
Compare
|
Review at 5094fe4, answered in 060ff68 (the branch was rewritten upstream with identical trees; 5094fe4 is now e9c2486):
Dmitrii Fedorov (@dmitrii-f-t27) |
…tion Review on #907: writeCredit refuses an owner or operator that is not a handle (letters, digits, - and _), but register accepted any owner, so a node registered as "developer 1" earned work and every credit became not_recorded. With a journal open, register now returns InvalidOwner, and openJournal refuses an operator or an already-registered owner that is not a handle (InvalidHandle). zig test src/trinet/agent.zig -lc: 68/68; removing any of the three checks fails the new test. Refs #906
|
At head 61 of 63 cases pass. Both failures, "main's build-matrix.yml before the fix" and "main's Those two cases load their input from the historical object Failing run: trinity-fpga/tools/test_check_run_syntax.py Line 114 in 060ff68 Could you reconcile those historical inputs, by pinning or retaining their content or holding Separately, Dmitrii Fedorov (@dmitrii-f-t27) |
…paid set before the credit is written Two accounting defects in the journal from #907, both of which break the spec's "one credit per (node, nonce) across restarts" (gHashTag/t27 specs/trinet/node-work-credit.t27): - Torn tail. A run that died mid-line left an unfinished last line, and the next run opened the journal and appended with fopen("a"), gluing its first mark onto the tear. Depending on where the tear was, the glued line either stops parsing (the journal becomes unreadable, crediting stops for good) or parses as a credit and swallows the mark (the next start resumes at an older mark and reissues nonces). `Ledger.openJournal` now returns JournalTornTail before any nonce or credit and leaves the file untouched; read-only `restore` still reports `torn_tail` for inspection. No automatic truncation. - Write before credit. The paid set grew after the credit line was fsynced, so an allocation failure left a durable credit this run never marked paid, and a second settle of the same (node, nonce) could write a second line. The room is now reserved before the write; the insert after it cannot fail. The spent key comes from journal.spentKey, the same function restore uses. Tests: zig test src/trinet/agent.zig -lc, 75/75 (Zig 0.16.0, macOS arm64). New: torn tail refused twice and left byte-identical, glued-line corruption reproduced, malformed line stops the coordinator, unverifiable and tampered receipts never reach the journal, a refused credit is neither paid nor marked paid, no credit reaches disk without room in the paid set, every credit line names its own node's owner. Refs #906
Closes #906
Defect. At ea895b5,
ledger.zigkeeps the paid (node, nonce) set in memory andmesh.zigstartsnext_nonceat 1 on every start. A restarted coordinator pays the same (node, nonce) again. Reproduced on main: two coordinator runs, one honest emulated node, nonce 1 credited in both, 2 mTRI for one nonce. A receipt saved from an earlier run can also match a reissued nonce.Fix. Spec first: gHashTag/t27#8426, PR gHashTag/t27#8427 (
specs/trinet/node-work-credit.t27).src/trinet/journal.zig(new): the coordinator's durable record, JSON lines.NonceSpaceExhausted, never a wrap.JournalTornTail(see e884a98 below).self_reportedwhen the coordinator's operator is the node's owner or unknown,verifiedotherwise.ledger.zig:openJournal, write-before-credit insettle, new outcomenot_recorded(not credited, not slashed: the fault is ours).mesh.zig:openJournal, a journal-backedfreshNonce(nowError!u32), and a nonce this run did not issue isnot_recorded.main.zig:TRINET_JOURNAL,TRINET_OWNER(defaultoperator, as before) andTRINET_OPERATORforfleet,demoandagent. WithoutTRINET_JOURNALnothing changes..github/workflows/conformance-frame-alignment.yml: thetrinet-testsjob runszig test src/trinet/agent.zig -lcbut did not trigger onsrc/trinet/**; it does now.Checks run locally (Zig 0.16.0, macOS arm64):
zig test src/trinet/agent.zig -lc: 67/67 pass, 11 new (6 journal, 5 mesh: restart, owner level, forged answer, unwritable journal, unreadable journal).zig fmt --checkon the four files: clean.demo /nonexistent-no-board): run 1 credits 288 jobs; run 2 restores 288, resumes at nonce 1024, credits 288 more. Journal: 576 credits, 576 unique (node, nonce), 0 duplicates, marks 1024 and 2048, allself_reported, allphysical:false.Not in this PR. No payout path: a closed run still needs an attestation source the signers accept (the spec's open trust model). The tag still does not prove a board (DEVICE_BOUND_IDENTITY). fsync per credit costs throughput at UART rates; batching the fsync per batch is a follow-up.
Update at e884a98: two accounting defects fixed
Both defects broke the spec's "one credit per (node, nonce) across restarts". The fix is code only. The spec already has the rules:
may_start_crediting, write-before-credit, andmay_credit(..., !key_spent, journal_written). Before this commit the code did not meet them. No spec change is needed.fopen("a"), so its first mark was glued onto the torn line. If the line was cut early, the glued line does not parse: the journal becomes unreadable and crediting stops for good. If it was cut later, the glued line parses as a credit and swallows the mark: the next start resumes at an older mark and reissues nonces. NowLedger.openJournalreturnsJournalTornTailbefore any nonce or credit, and the file stays byte-identical.restore(read-only) still reportstorn_tail. Nothing is truncated automatically.main.zigtells the operator what happened.settleof the same (node, nonce) could then write a second line. Now the room is reserved before the write, and the insert after the write cannot fail. The spent key comes fromjournal.spentKey, the functionrestoreuses.Checks at e884a98, run locally (Zig 0.16.0, macOS arm64):
zig test src/trinet/agent.zig -lc: 75/75 pass. The 7 new tests cover (68 at 060ff68):zig fmt --check src/trinet/*.zig: clean.Decision: self_reported records
Every accepted job is recorded at the settlement rate (1 mTRI) against the node owner's handle,
self_reportedincluded. This keeps today's behaviour, and nothing done on our own nodes is lost from the record. It does not open a payout or a double payment:node-work-credit.t27) gates value on the level, not on the record.creditable(SELF_REPORTED)is false.may_request_attestationneedsLEVEL_VERIFIED.mintableneedsLEVEL_ATTESTED. Every line carries itslevel, so whatever reads the journal for a payout must apply those gates. Aself_reportedline is the run's tally, not a payable balance.Dmitrii Fedorov (@dmitrii-f-t27)