OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / services / builtin / shard-store / shard-recover.js

53 lines · javascript ref:
/**
 * Client-side shard recovery + discovery helpers (M4).
 *
 * These run on the recovering client, not the relay. Given a signed custody
 * shareManifest, fetch >= shareThreshold shards by hash from whichever relays
 * hold them, verify each by re-hashing (content-address integrity — no relay
 * is trusted), and return the collected shares for PVSS reconstruction.
 */
import sodium from 'sodium-universal'
import b4a from 'b4a'
import { normalizeShardAddress, shardHash } from './shard-engine.js'

/** DHT topic a holder announces on so a recovering client can find it. */
export function shardAnnounceTopic (address) {
  const hash = normalizeShardAddress(address)
  if (!hash) throw new Error('shardAnnounceTopic: invalid shard address')
  const out = b4a.alloc(32)
  sodium.crypto_generichash(out, b4a.from('shard\0' + hash, 'utf8'))
  return out // 32-byte topic
}

/**
 * @param manifest        [{ shareIndex, shard }] from a verified custody-intent
 * @param shareThreshold  k — how many distinct valid shares are needed
 * @param fetch           async (shardAddress) => bytes | null  (hits relays)
 * @returns { ok, collected: [{ shareIndex, shard, bytes }], missing: [...], enough }
 */
export async function recoverShards ({ manifest, shareThreshold, fetch } = {}) {
  if (!Array.isArray(manifest)) throw new Error('recoverShards: manifest array required')
  if (typeof fetch !== 'function') throw new Error('recoverShards: fetch(shard) required')
  const need = Number.isInteger(shareThreshold) && shareThreshold > 0 ? shareThreshold : manifest.length
  const collected = []
  const missing = []
  for (const entry of manifest) {
    if (collected.length >= need) break
    const hash = normalizeShardAddress(entry.shard)
    if (!hash) { missing.push(entry); continue }
    let bytes = null
    try {
      bytes = await fetch('shard:' + hash)
    } catch {
      bytes = null
    }
    // content-address integrity: a relay returning wrong bytes is caught here.
    if (bytes && shardHash(bytes) === hash) {
      collected.push({ shareIndex: entry.shareIndex, shard: 'shard:' + hash, bytes })
    } else {
      missing.push(entry)
    }
  }
  return { ok: collected.length >= need, enough: collected.length >= need, collected, missing }
}