OverviewFilesCommitsIssuesDocsReleasesPeers

hypercompute / lib / node.js

136 lines · javascript ref:
// lib/node.js — ComputeNode: owns the corestore + durable-plane Autobase and
// the T0 (trusted, native) executor table. SPEC §2, §4.
//
// P0 scope: enrollment/transport are scaffolded (see enroll.js / transport.js)
// but the *core* — content-addressed submit -> execute -> result over the
// deterministic fold, with memoization — is real and exercised by test/e2e.js
// via replicateAndSync (no network).

const Corestore = require('corestore')
const b4a = require('b4a')
const crypto = require('hypercore-crypto')
const { openPool, taskId, resultKey, DEFAULT_LIMITS } = require('./pool')

// a leased claim is expired once the worker's own clock passes at + leaseMs.
// Reading the clock here (scheduling) is fine; apply() must never do this.
function claimExpired (claim) {
  if (!claim || typeof claim.at !== 'number' || typeof claim.leaseMs !== 'number') return true
  return Date.now() > claim.at + claim.leaseMs
}

// resolve `promise` unless `ms` elapses first (the wallMs bound). The underlying
// executor is not forcibly killed (a real WASM sandbox would be); we stop awaiting.
function withTimeout (promise, ms) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('wallMs timeout')), ms)
    promise.then(
      (v) => { clearTimeout(timer); resolve(v) },
      (e) => { clearTimeout(timer); reject(e) }
    )
  })
}

class ComputeNode {
  constructor (store, bootstrap, { identity, executors } = {}) {
    this.store = store
    this.identity = identity || crypto.keyPair()
    this.executors = executors || {} // codeHash -> async (input) => output   (T0 native)
    this.base = openPool(store, bootstrap)
  }

  // storage: a Corestore instance, or a path string (corestore 7 / hypercore 11
  // is RocksDB-backed via hypercore-storage — there is no random-access-memory
  // backend in this era; tests/demos pass a temp dir).
  static async create (storage, bootstrap = null, opts = {}) {
    const store = storage instanceof Corestore ? storage : new Corestore(storage)
    const node = new ComputeNode(store, bootstrap, opts)
    await node.base.ready()
    return node
  }

  get key () { return this.base.key } // pool bootstrap key (share to join)
  get writerKey () { return this.base.local.key } // this node's autobase writer key
  get idHex () { return b4a.toString(this.identity.publicKey, 'hex') }

  // --- membership (P0: direct; real path is enroll.js blind-pairing) ----------
  async grantSelf (role = 'worker') {
    await this.base.append({ type: 'grant', identityPubKey: this.idHex, role })
  }

  async grant (identityPubKey, role = 'worker') {
    const hex = b4a.isBuffer(identityPubKey) ? b4a.toString(identityPubKey, 'hex') : identityPubKey
    await this.base.append({ type: 'grant', identityPubKey: hex, role })
  }

  async addWriter (writerKey, indexer = false) {
    const hex = b4a.isBuffer(writerKey) ? b4a.toString(writerKey, 'hex') : writerKey
    await this.base.append({ type: 'add-writer', writerKey: hex, indexer })
  }

  // --- content-addressed compute (the core, SPEC §4) --------------------------
  async lookup (codeHash, inputHash) {
    const node = await this.base.view.get(resultKey(codeHash, inputHash))
    return node ? node.value.output : null
  }

  // submit returns { output, memoHit:true } on a cache hit, else { id, memoHit:false }.
  // opts.limits ({ fuel, wallMs, memPages }) bound the task (SPEC §4.3); opts.tier
  // is the trust requirement (SPEC §4.4; v0.1 = T0).
  async submit (codeHash, inputHash, input, { limits, tier = 'T0' } = {}) {
    const cached = await this.lookup(codeHash, inputHash)
    if (cached !== null && cached !== undefined) return { output: cached, memoHit: true }
    const lim = limits || DEFAULT_LIMITS
    const id = taskId(codeHash, inputHash, lim)
    await this.base.append({ type: 'task', id, codeHash, inputHash, input, limits: lim, tier })
    return { id, memoHit: false }
  }

  // worker loop: execute every task that has a registered executor and no result
  // yet, respecting other workers' leased claims. Returns the number executed.
  async runPendingTasks () {
    let executed = 0
    const tasks = []
    for await (const { value: task } of this.base.view.createReadStream({ gte: 'task/', lt: 'task0' })) {
      tasks.push(task)
    }
    for (const task of tasks) {
      const existing = await this.base.view.get(resultKey(task.codeHash, task.inputHash))
      if (existing) continue
      const exec = this.executors[task.codeHash]
      if (!exec) continue

      // leased soft-lock (SPEC §6): skip a task another worker holds an UNEXPIRED
      // claim on. Best-effort dedup — simultaneous claims still race (wasted, not
      // corrupt: results are content-keyed + write-once). The clock is read HERE,
      // in the scheduler, never in apply() — the fold stays deterministic (SPEC §3).
      const claim = await this.base.view.get('claim/' + task.id)
      if (claim && claim.value.worker !== this.idHex && !claimExpired(claim.value)) continue

      const leaseMs = (task.limits && task.limits.wallMs) || DEFAULT_LIMITS.wallMs
      await this.base.append({ type: 'claim', id: task.id, worker: this.idHex, at: Date.now(), leaseMs })

      let output
      try {
        // wallMs is the one bound enforceable without a sandbox (anti-DoS, SPEC
        // §4.3); fuel/memPages are enforced by the WASM sandbox (P2).
        output = await withTimeout(exec(task.input), leaseMs) // T0 native execution
      } catch {
        continue // timeout or executor error — leave unexecuted for retry / another worker
      }
      await this.base.append({
        type: 'result',
        codeHash: task.codeHash,
        inputHash: task.inputHash,
        output,
        worker: this.idHex
      })
      executed++
    }
    return executed
  }

  close () { return this.base.close() }
}

module.exports = { ComputeNode }