OverviewFilesCommitsIssuesDocsReleasesPeers

hypercompute commits / fab0099

fix: make memoized result write-once (first accepted result is canonical)
apply()'s result case did an unconditional view.put, so any writer could append a second result for the same (codeHash, inputHash) and clobber the memoized chunk (last-write-wins) — undermining "the first correct computation is shared by everyone" (SPEC 4.1). The fold is deterministic across peers, so "first result in linearized order" is a canonical choice every peer agrees on. Guard the put: only write if no result exists for the key. Full k-of-n voting is still P3; this is the P0 floor that makes a memoized chunk trustworthy. test/result-write-once.js: two peers, owner writes the canonical result, a second peer appends a conflicting result for the same key; assert the first wins on BOTH peers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fab0099421201207efa6f5a35e545382f64dad2f
by plankatron <fleeky@gmail.com> · 2026-06-17T22:56:37.000Z

Changed files (2) +52 -2

lib/pool.js +9 -2
……@@ -56,9 +56,16 @@ function openPool (store, bootstrap) {
5656 case 'claim': // optimistic soft-lock to dedupe workers
5757 await view.put('claim/' + op.id, { worker: op.worker })
5858 break
59 case 'result': // memoized output, keyed by content
60 await view.put(resultKey(op.codeHash, op.inputHash), op)
59 case 'result': { // memoized output, keyed by content — WRITE-ONCE
60 // The fold is deterministic across peers, so "first result in
61 // linearized order" is a canonical choice every peer agrees on. A
62 // later conflicting result for the same (codeHash, inputHash) — from
63 // a buggy / non-deterministic / malicious worker — must NOT clobber
64 // the canonical chunk. (Full k-of-n voting is P3; this is the P0 floor.)
65 const rk = resultKey(op.codeHash, op.inputHash)
66 if (!(await view.get(rk))) await view.put(rk, op)
6167 break
68 }
6269 }
6370 }
6471 }
test/result-write-once.js +43 -0
……@@ -0,0 +1,43 @@
1// test/result-write-once.js — the canonical result is WRITE-ONCE.
2//
3// Two peers share a pool. The owner writes the canonical result for a content
4// key; a second peer then appends a CONFLICTING result for the SAME
5// (codeHash, inputHash). The fold must keep the first (canonical) result on
6// BOTH peers — the conflict is ignored, not applied last-write-wins. This is
7// the P0 floor that makes a memoized chunk trustworthy (SPEC §4.1).
8
9const test = require('brittle')
10const Corestore = require('corestore')
11const tmp = require('test-tmp')
12const { replicateAndSync } = require('autobase-test-helpers')
13const { ComputeNode } = require('../lib/node')
14
15const codeHash = 'square-v1'
16const inputHash = 'deadbeefcafe'
17
18test('result is write-once: a later conflicting result cannot clobber the canonical chunk', async (t) => {
19 const owner = await ComputeNode.create(new Corestore(await tmp(t)), null, {})
20 await owner.grantSelf('owner')
21
22 const worker = await ComputeNode.create(new Corestore(await tmp(t)), owner.key, {})
23 await owner.addWriter(worker.writerKey)
24 await replicateAndSync([owner.base, worker.base])
25 t.ok(worker.base.writable, 'worker became a writer')
26
27 // owner writes the canonical result first, and it fully linearizes before
28 // any conflict exists (so causal order pins it first in the fold).
29 await owner.base.append({ type: 'result', codeHash, inputHash, output: 49, worker: owner.idHex })
30 await replicateAndSync([owner.base, worker.base])
31 t.is(await owner.lookup(codeHash, inputHash), 49, 'canonical result present after first write')
32
33 // worker appends a CONFLICTING result for the same content key
34 await worker.base.append({ type: 'result', codeHash, inputHash, output: 999, worker: worker.idHex })
35 await replicateAndSync([owner.base, worker.base])
36
37 // write-once: the first result wins on BOTH peers; the conflict is dropped
38 t.is(await owner.lookup(codeHash, inputHash), 49, 'owner: canonical result NOT clobbered')
39 t.is(await worker.lookup(codeHash, inputHash), 49, 'worker: canonical result NOT clobbered')
40
41 await owner.close()
42 await worker.close()
43})