// lib/pool.js — the DURABLE plane (SPEC §2, §4).
//
// One optimistic Autobase whose view is a Hyperbee. Mutable state = a
// deterministic fold (`apply`) over the append-only op log. View keys:
// member/<idPub> -> { role }
// task/<id> -> task op { codeHash, inputHash, input, limits, tier }
// claim/<id> -> { worker, at, leaseMs } (leased soft-lock)
// result/<codeHash>/<input> -> result op (the memoized chunk)
//
// apply() MUST stay deterministic (no Date.now/random/external reads) — SPEC §3.
const Autobase = require('autobase')
const Hyperbee = require('hyperbee')
const b4a = require('b4a')
const crypto = require('hypercore-crypto')
function hash (str) {
return b4a.toString(crypto.hash(b4a.from(str)), 'hex')
}
// resource bounds (SPEC §4.3). fuel/memPages are enforced by the WASM sandbox
// (P2); wallMs is enforceable today as a wall-clock timeout (see node.js).
const DEFAULT_LIMITS = { fuel: 1000000000, wallMs: 30000, memPages: 16384 }
// byte-stable encoding of limits (fixed field order) so the task id is identical
// on every peer (content-addressing requires it).
function encodeLimits (limits) {
const l = limits || DEFAULT_LIMITS
return l.fuel + '|' + l.wallMs + '|' + l.memPages
}
// content-addressed identity of a unit of work (SPEC §4.1:
// id = hash(codeHash ‖ inputHash ‖ limits)). limits ARE part of the identity — a
// different budget is a different task — but the RESULT is keyed by
// (codeHash, inputHash) only: the same code+input yields the same output
// regardless of budget.
function taskId (codeHash, inputHash, limits) {
return hash(codeHash + ':' + inputHash + ':' + encodeLimits(limits))
}
function resultKey (codeHash, inputHash) {
return 'result/' + codeHash + '/' + inputHash
}
function openPool (store, bootstrap) {
return new Autobase(store, bootstrap, {
valueEncoding: 'json',
open (s) {
return new Hyperbee(s.get('view'), {
keyEncoding: 'utf-8',
valueEncoding: 'json'
})
},
// the keet pattern (SPEC §2): optimistic writes never block on quorum
optimistic: true,
ackInterval: 1000,
ackThreshold: 4,
async apply (nodes, view, host) {
for (const node of nodes) {
const op = node.value
switch (op && op.type) {
case 'add-writer':
await host.addWriter(b4a.from(op.writerKey, 'hex'), { indexer: !!op.indexer })
break
case 'grant': // enrollment result (SPEC §4-enroll)
await view.put('member/' + op.identityPubKey, { role: op.role })
break
case 'task': // content-addressed work unit
await view.put('task/' + op.id, op)
break
case 'claim': // leased soft-lock to dedupe workers (SPEC §6).
// apply only STORES the lease fields the worker stamped — it never
// reads the clock itself, so the fold stays deterministic (SPEC §3).
await view.put('claim/' + op.id, { worker: op.worker, at: op.at, leaseMs: op.leaseMs })
break
case 'result': { // memoized output, keyed by content — WRITE-ONCE
// The fold is deterministic across peers, so "first result in
// linearized order" is a canonical choice every peer agrees on. A
// later conflicting result for the same (codeHash, inputHash) — from
// a buggy / non-deterministic / malicious worker — must NOT clobber
// the canonical chunk. (Full k-of-n voting is P3; this is the P0 floor.)
const rk = resultKey(op.codeHash, op.inputHash)
if (!(await view.get(rk))) await view.put(rk, op)
break
}
}
}
}
})
}
module.exports = { openPool, taskId, resultKey, hash, DEFAULT_LIMITS }