OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / integration / blind-shard-dispersal-e2e.test.js

182 lines · javascript ref:
/**
 * BETA 1.0 PROOF — blind custody over the real shard store.
 *
 * A dealer PVSS-splits a secret (e.g. a public app's content key) into n shares
 * and disperses each as an opaque shard to a DISTINCT relay's real
 * ShardStoreService, authorized by a signed custody pin. Then:
 *
 *   - each relay holds exactly ONE opaque, content-addressed shard;
 *   - NO SINGLE relay (and no k-1 colluding relays) can reconstruct the secret;
 *   - ANY k relays let a reader reconstruct the exact secret at its edge.
 *
 * This exercises the actual relay authorization path (authorizeShardPin against
 * a custody assignment) + content-addressed storage + the client blind-shard
 * codec + PVSS reconstruction — end to end, no mocks of the crypto or store.
 */
import test from 'brittle'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import Corestore from 'corestore'
import b4a from 'b4a'
import sodium from 'sodium-universal'
import {
  ShardStoreService, signShardPin, normalizeShardAddress
} from '../../packages/services/builtin/shard-store/index.js'
import {
  disperseSecret, recoverSecret, shardAddressOf
} from '../../packages/client/blind-shards.js'

function keyPair (seed) {
  const publicKey = b4a.alloc(32)
  const secretKey = b4a.alloc(64)
  sodium.crypto_sign_seed_keypair(publicKey, secretKey, b4a.alloc(32, seed))
  return { publicKey, secretKey, hex: b4a.toString(publicKey, 'hex') }
}

async function tmpStore (t) {
  const dir = await mkdtemp(join(tmpdir(), 'blind-shard-e2e-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const store = new Corestore(dir)
  t.teardown(() => store.close())
  return store
}

// One relay = a real ShardStoreService. Its custody resolver returns whatever
// share the dealer is assigning it right now (set just-in-time in put()), i.e.
// the relay authorizes exactly the one blob the signed manifest binds to it.
async function makeRelay (t, seed) {
  const store = await tmpStore(t)
  const kp = keyPair(seed)
  const relay = { kp, assigned: null, svc: null }
  relay.svc = new ShardStoreService({
    putAuth: ['custody'],
    resolveCustodyAssignment: async () => relay.assigned
  })
  await relay.svc.start({ store, node: { keyPair: kp } })
  t.teardown(() => relay.svc.stop())
  return relay
}

const INTENT_ID = 'a1b2'.repeat(16) // 64-hex custody intent id
const nonceFor = (i) => i.toString(16).padStart(32, '0')

// The dealer's PUT: bind the share to its assigned relay, sign a custody pin,
// and store it through the relay's REAL put() (which verifies the pin).
function dealerPut (relays, dealerKp) {
  return async (bytes, { shareIndex }) => {
    const address = shardAddressOf(bytes)
    const hash = normalizeShardAddress(address)
    const relay = relays[shareIndex - 1]
    relay.assigned = { shareIndex, shard: address }
    const pin = signShardPin({
      reason: 'custody',
      hash,
      custodyIntentId: INTENT_ID,
      shareIndex,
      retainUntil: Date.now() + 3600000,
      nonce: nonceFor(shareIndex)
    }, dealerKp)
    const res = await relay.svc.put({ ciphertext: b4a.toString(bytes, 'base64'), pin })
    return res.shard
  }
}

// A reader's fetch limited to a subset of relays (models "only these answer").
function readerFetch (relaySubset) {
  return async (address) => {
    const hash = normalizeShardAddress(address)
    for (const relay of relaySubset) {
      try {
        const has = await relay.svc.has({ hash })
        if (has && has.present) {
          const got = await relay.svc.get({ hash })
          return b4a.from(got.ciphertext, 'base64')
        }
      } catch { /* relay doesn't hold it */ }
    }
    return null
  }
}

test('blind custody E2E: 3-of-5 dispersal, no single relay reconstructs, reader rebuilds', async (t) => {
  const relays = []
  for (let i = 1; i <= 5; i++) relays.push(await makeRelay(t, i))
  const dealer = keyPair(200)

  // 1. Dealer disperses a secret across the 5 real relays.
  const dispersed = await disperseSecret({ count: 5, threshold: 3, put: dealerPut(relays, dealer) })
  t.is(dispersed.shareManifest.length, 5)

  // 2. Each relay holds exactly ONE opaque shard (placement: none has >= k).
  for (let i = 0; i < relays.length; i++) {
    let held = 0
    for (const entry of dispersed.shareManifest) {
      const has = await relays[i].svc.has({ hash: normalizeShardAddress(entry.shard) })
      if (has && has.present) held++
    }
    t.is(held, 1, 'relay ' + (i + 1) + ' holds exactly one of the five shards')
  }

  // 3. INVARIANT — a single relay cannot reconstruct.
  const one = await recoverSecret({ shareManifest: dispersed.shareManifest, threshold: 3, fetch: readerFetch([relays[0]]) })
  t.is(one.ok, false, 'one relay: not enough')
  t.is(one.collected, 1)

  // 4. INVARIANT — k-1 colluding relays still cannot reconstruct.
  const collude = await recoverSecret({ shareManifest: dispersed.shareManifest, threshold: 3, fetch: readerFetch([relays[0], relays[1]]) })
  t.is(collude.ok, false, 'two relays: still not enough')

  // 5. A reader gathering ANY k relays reconstructs the exact secret at the edge.
  const reader = await recoverSecret({
    shareManifest: dispersed.shareManifest,
    threshold: 3,
    fetch: readerFetch([relays[1], relays[3], relays[4]]) // shares 2,4,5
  })
  t.is(reader.ok, true, 'any 3 relays: reconstructs')
  t.is(reader.key, dispersed.key, 'reader recovers the dealer-private key — no relay ever held it')
  t.is(reader.secretPoint, dispersed.secretPoint)

  // 6. A reader with all relays available also works (and still stops at k).
  const full = await recoverSecret({ shareManifest: dispersed.shareManifest, threshold: 3, fetch: readerFetch(relays) })
  t.is(full.ok, true)
  t.is(full.key, dispersed.key)
})

test('relay rejects a custody pin it was not assigned', async (t) => {
  const relay = await makeRelay(t, 42)
  const dealer = keyPair(201)
  const bytes = b4a.from('an-opaque-share-blob', 'utf8')
  const address = shardAddressOf(bytes)
  const hash = normalizeShardAddress(address)

  // relay is assigned share 3 -> this hash
  relay.assigned = { shareIndex: 3, shard: address }

  // a pin claiming share 4 (not this relay's assignment) must be refused
  const wrongIndexPin = signShardPin({
    reason: 'custody',
    hash,
    custodyIntentId: INTENT_ID,
    shareIndex: 4,
    retainUntil: Date.now() + 3600000,
    nonce: nonceFor(99)
  }, dealer)
  await t.exception(
    relay.svc.put({ ciphertext: b4a.toString(bytes, 'base64'), pin: wrongIndexPin }),
    /UNAUTHORIZED_PIN/
  )

  // the correctly-assigned pin is accepted
  const okPin = signShardPin({
    reason: 'custody',
    hash,
    custodyIntentId: INTENT_ID,
    shareIndex: 3,
    retainUntil: Date.now() + 3600000,
    nonce: nonceFor(3)
  }, dealer)
  const res = await relay.svc.put({ ciphertext: b4a.toString(bytes, 'base64'), pin: okPin })
  t.is(res.shard, address)
})