OverviewFilesCommitsIssuesDocsReleasesPeers

hypercompute / lib / pool.js

93 lines · javascript ref:
// 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 }