OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-inspector / lib / core-enumerator.js

252 lines · javascript ref:
import RocksDB from 'rocksdb-native'
import b4a from 'b4a'

// Mirrors hypercore-storage key encoding
// TL_CORE_BY_DKEY = 1, keys are: varint(1) + fixed32(discoveryKey)
// Values are @corestore/core: { version, corePointer, dataPointer, alias? }

const CORESTORE_CF = 'corestore'

export async function enumerateCores (dbPath, opts = {}) {
  const { readOnly = false } = opts

  const db = new RocksDB(dbPath, {
    readOnly,
    columnFamilies: [CORESTORE_CF]
  })
  await db.ready()

  const cs = db.columnFamily(CORESTORE_CF)
  const cores = []

  // Key prefix 0x01 = TL_CORE_BY_DKEY (varint-encoded 1)
  // Each entry maps a discovery key to a @corestore/core record
  const start = Buffer.from([0x01])
  const end = Buffer.from([0x02])

  const iter = cs.iterator({ gte: start, lt: end, limit: 10000 })

  await new Promise((resolve, reject) => {
    iter.on('data', ({ key, value }) => {
      if (!value || key[0] !== 0x01) return

      // Discovery key is the 32 bytes after the varint prefix
      const discoveryKey = b4a.toString(key.subarray(1, 33), 'hex')

      // Decode @corestore/core value using compact-encoding
      try {
        const core = decodeCoreRecord(value)
        cores.push({
          discoveryKey,
          ...core
        })
      } catch (err) {
        cores.push({
          discoveryKey,
          corePointer: -1,
          dataPointer: -1,
          alias: null,
          error: err.message
        })
      }
    })
    iter.on('end', resolve)
    iter.on('error', reject)
  })

  // Read auth records to get public keys and head lengths
  // Auth key format: varint(3) + varint(corePointer) + varint(0)
  // Auth value: fixed32(key) + fixed32(discoveryKey) + varint(flags) + ...
  // Head key format: varint(4) + varint(dataPointer) + varint(0)
  // Head value: { fork, length, rootHash, signature }
  for (const core of cores) {
    // Build auth key
    const authKey = encodeKey(3, core.corePointer, 0)
    try {
      const authIter = cs.iterator({ gte: authKey, lt: incrementKey(authKey), limit: 1 })
      await new Promise((resolve, reject) => {
        authIter.on('data', ({ value }) => {
          if (value && value.length >= 64) {
            core.publicKey = b4a.toString(value.subarray(0, 32), 'hex')
          }
        })
        authIter.on('end', resolve)
        authIter.on('error', reject)
      })
    } catch {}

    // Build head key to get core length
    const headKey = encodeKey(4, core.dataPointer, 0)
    try {
      const headIter = cs.iterator({ gte: headKey, lt: incrementKey(headKey), limit: 1 })
      await new Promise((resolve, reject) => {
        headIter.on('data', ({ value }) => {
          if (value) {
            const head = decodeHeadRecord(value)
            core.length = head.length
            core.fork = head.fork
          }
        })
        headIter.on('end', resolve)
        headIter.on('error', reject)
      })
    } catch {}
  }

  // Also try to read the corestore head for allocation counts
  let head = null
  try {
    const headKey = Buffer.from([0x00])
    const headIter = cs.iterator({ gte: headKey, lt: Buffer.from([0x01]), limit: 1 })
    await new Promise((resolve, reject) => {
      headIter.on('data', ({ value }) => {
        if (value) head = decodeHead(value)
      })
      headIter.on('end', resolve)
      headIter.on('error', reject)
    })
  } catch {}

  if (head) {
    cores._head = head
  }

  await db.close()

  // Sort by corePointer
  cores.sort((a, b) => a.corePointer - b.corePointer)

  return cores
}

// Encode a RocksDB key from varint parts
function encodeKey (...parts) {
  const bufs = parts.map(encodeVarint)
  return Buffer.concat(bufs)
}

function encodeVarint (n) {
  const bytes = []
  while (n >= 0x80) {
    bytes.push((n & 0x7f) | 0x80)
    n >>>= 7
  }
  bytes.push(n & 0x7f)
  return Buffer.from(bytes)
}

function incrementKey (key) {
  const buf = Buffer.from(key)
  for (let i = buf.length - 1; i >= 0; i--) {
    if (buf[i] < 0xff) {
      buf[i]++
      return buf
    }
    buf[i] = 0
  }
  return Buffer.concat([Buffer.from([0x01]), buf])
}

// Decode @core/head { fork, length, rootHash, signature }
function decodeHeadRecord (buf) {
  let pos = 0
  const { value: fork, bytesRead: fb } = readVarint(buf, pos)
  pos += fb
  const { value: length, bytesRead: lb } = readVarint(buf, pos)
  pos += lb
  return { fork, length }
}

// Minimal varint decoder
function readVarint (buf, offset) {
  let result = 0
  let shift = 0
  let pos = offset
  while (pos < buf.length) {
    const byte = buf[pos]
    result |= (byte & 0x7f) << shift
    pos++
    if ((byte & 0x80) === 0) break
    shift += 7
  }
  return { value: result, bytesRead: pos - offset }
}

function readFixed32 (buf, offset) {
  return {
    value: b4a.toString(buf.subarray(offset, offset + 32), 'hex'),
    bytesRead: 32
  }
}

function readString (buf, offset) {
  const { value: len, bytesRead: lenBytes } = readVarint(buf, offset)
  const str = b4a.toString(buf.subarray(offset + lenBytes, offset + lenBytes + len), 'utf8')
  return { value: str, bytesRead: lenBytes + len }
}

// Decode @corestore/core { version, corePointer, dataPointer, alias? }
function decodeCoreRecord (buf) {
  let pos = 0

  const { value: version, bytesRead: vb } = readVarint(buf, pos)
  pos += vb

  const { value: corePointer, bytesRead: cpb } = readVarint(buf, pos)
  pos += cpb

  const { value: dataPointer, bytesRead: dpb } = readVarint(buf, pos)
  pos += dpb

  const { value: flags, bytesRead: fb } = readVarint(buf, pos)
  pos += fb

  let alias = null
  if (flags & 1) {
    // @corestore/alias { name, namespace }
    const { value: name, bytesRead: nb } = readString(buf, pos)
    pos += nb
    const { value: namespace, bytesRead: nsb } = readFixed32(buf, pos)
    pos += nsb
    alias = { name, namespace }
  }

  return { version, corePointer, dataPointer, alias }
}

// Decode @corestore/head { version, flags, allocated?, seed?, defaultDiscoveryKey? }
function decodeHead (buf) {
  let pos = 0

  const { value: version, bytesRead: vb } = readVarint(buf, pos)
  pos += vb

  const { value: flags, bytesRead: fb } = readVarint(buf, pos)
  pos += fb

  let allocated = null
  if (flags & 1) {
    const { value: cores, bytesRead: cb } = readVarint(buf, pos)
    pos += cb
    const { value: datas, bytesRead: db } = readVarint(buf, pos)
    pos += db
    allocated = { cores, datas }
  }

  let seed = null
  if (flags & 2) {
    const { value, bytesRead } = readFixed32(buf, pos)
    pos += bytesRead
    seed = value
  }

  let defaultDiscoveryKey = null
  if (flags & 4) {
    const { value, bytesRead } = readFixed32(buf, pos)
    pos += bytesRead
    defaultDiscoveryKey = value
  }

  return { version, allocated, seed, defaultDiscoveryKey }
}