OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-vdrive / test / gc-bitfield.mjs

153 lines · javascript ref:
// Step 2 — GC / bitfield pairing.
// Q1: after the overlay drops bytes, does the core still advertise the block?
// Q2: what does a PEER experience when it requests an advertised-but-missing block?
// Q3: does core.clear() on that range make the lie go away?
import { createRequire } from 'module'
const require = createRequire(import.meta.url)

const fs = require('fs')
const crypto = require('hypercore-crypto')
const Corestore = require('corestore')
const Hyperdrive = require('hyperdrive')
const { ContentStore, build, stats } = require('../lib/storage.cjs')
const { chunk } = await import('../../hyper-overlay/lib/chunker.js')

const ROOT = process.argv[2]

// A block we advertise but cannot produce surfaces as an UNCAUGHT REQUEST_TIMEOUT
// from the replicator's timer, not as a rejection of get(). Capture it so the
// probe can finish and report rather than dying.
const uncaught = []
process.on('uncaughtException', (err) => {
  uncaught.push(err.code || err.message)
})

const srcDir = ROOT + '/src'
fs.mkdirSync(srcDir, { recursive: true })

const FILES = [
  { name: '/keep.bin', bytes: crypto.randomBytes(3 * 1024 * 1024) },
  { name: '/victim.bin', bytes: crypto.randomBytes(3 * 1024 * 1024) }
]
for (const f of FILES) {
  f.file = srcDir + f.name
  fs.writeFileSync(f.file, f.bytes)
  f.chunks = chunk(f.bytes)
}

const content = new ContentStore()
for (const f of FILES) for (const c of f.chunks) content.add(c.hash, f.file, c.offset, c.length)

const Storage = build(content)
const hostDir = ROOT + '/host'

function write (ws, buf) {
  return new Promise((resolve) => {
    if (ws.write(buf)) return resolve()
    ws.once('drain', resolve)
  })
}

async function openHost () {
  const store = new Corestore(new Storage(hostDir), { primaryKey: Buffer.alloc(32, 7), unsafe: true })
  const drive = new Hyperdrive(store)
  await drive.ready()
  await drive.getBlobs()
  return { store, drive }
}

// publish
{
  const { store, drive } = await openHost()
  for (const f of FILES) {
    const ws = drive.createWriteStream(f.name, { dedup: true })
    for (const c of f.chunks) await write(ws, c.data)
    await new Promise((resolve, reject) => {
      ws.once('error', reject)
      ws.once('close', resolve)
      ws.end()
    })
  }
  await drive.close()
  await store.close()
}

const host = await openHost()
const driveKey = host.drive.key

// which blocks belong to /victim.bin?
const victimEntry = await host.drive.entry('/victim.bin')
const victimBlob = victimEntry.value.blob
const victimMap = await host.drive.blobs.getBlockMap(victimBlob)
const victimBlocks = victimMap.blocks.map((b) => b.index)

// peer setup
const client = new Corestore(ROOT + '/peer', { primaryKey: Buffer.alloc(32, 9), unsafe: true })
const clientDrive = new Hyperdrive(client, driveKey)
await clientDrive.ready()
const s1 = host.store.replicate(true)
const s2 = client.replicate(false)
s1.pipe(s2).pipe(s1)
await clientDrive.core.update({ wait: true })

const result = {}

// baseline: peer can fetch a file normally
const keep = await clientDrive.get('/keep.bin')
result.peerFetchedKeepOk = keep !== null && keep.equals(FILES[0].bytes)

// Q1 — the overlay GCs victim.bin's bytes
for (const c of FILES[1].chunks) content.locations.delete(c.hash)
result.hostStillAdvertises_afterBytesGone = await host.drive.blobs.core.has(victimBlocks[0])

// Q2 — peer requests the advertised-but-missing block
const t0 = Date.now()
try {
  const got = await clientDrive.get('/victim.bin', { timeout: 5000 })
  result.peerResult = got === null ? 'null' : 'bytes:' + got.length
} catch (err) {
  result.peerResult = 'throw:' + (err.code || err.message)
}
result.peerWaitedMs = Date.now() - t0

// Q3 — pair the GC with a clear of those blocks
await host.drive.blobs.core.clear(victimBlocks[0], victimBlocks[victimBlocks.length - 1] + 1)
result.hostAdvertises_afterClear = await host.drive.blobs.core.has(victimBlocks[0])

// peer asks again, from a clean session
const client2 = new Corestore(ROOT + '/peer2', { primaryKey: Buffer.alloc(32, 11), unsafe: true })
const clientDrive2 = new Hyperdrive(client2, driveKey)
await clientDrive2.ready()
const s3 = host.store.replicate(true)
const s4 = client2.replicate(false)
s3.pipe(s4).pipe(s3)
await clientDrive2.core.update({ wait: true })

const t1 = Date.now()
try {
  const got = await clientDrive2.get('/victim.bin', { timeout: 5000 })
  result.peerResultAfterClear = got === null ? 'null' : 'bytes:' + got.length
} catch (err) {
  result.peerResultAfterClear = 'throw:' + (err.code || err.message)
}
result.peerWaitedMsAfterClear = Date.now() - t1

// and the file that was never GC'd still works
const keep2 = await clientDrive2.get('/keep.bin', { timeout: 5000 })
result.keepStillFetchableAfterClear = keep2 !== null && keep2.equals(FILES[0].bytes)

result.victimBlockCount = victimBlocks.length
result.stats = { ...stats }
result.uncaught = uncaught

for (const s of [s1, s2, s3, s4]) s.destroy()
await clientDrive2.close()
await client2.close()
await clientDrive.close()
await client.close()
await host.drive.close()
await host.store.close()
content.close()

console.log(JSON.stringify(result, null, 2))