OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-client / requests.js

277 lines · javascript ref:
import b4a from 'b4a'
import {
  CELL_SIZE_CLASS,
  FAMILY,
  OPERATION
} from '@hiverelay/blind-protocol/wire-runtime-authority'
import {
  allocationCommitment,
  blake2b256,
  cellBatchGetRequestCommitment,
  cellGetRequestCommitment,
  cellManageRequestCommitment,
  cellProveRequestCommitment,
  cellPutRequestCommitment,
  cellStorageSlot
} from '@hiverelay/blind-protocol/hashes'
import {
  batchGetV1,
  dropCellV1,
  getCellV1,
  proveCellV1,
  putCellV1,
  renewCellV1
} from '@hiverelay/blind-protocol/schemas'
import { encodeCanonical } from '@hiverelay/blind-protocol/codec'
import { asBytes, randomBytes, wipe } from './bytes.js'
import {
  destroyCellCapabilities,
  generateDistinctCapabilityKeys,
  signCapability
} from './capabilities.js'
import { sealCell } from './cells.js'
import { fail } from './errors.js'
import { resolveAdmission } from './provider.js'
import { selectedOperationProfile } from './selected-operation-profile.js'

function u32 (value, field) {
  if (!Number.isSafeInteger(value) || value < 0 || value > 0xffffffff) fail('BAD_CLIENT_INPUT', `${field} is outside u32`)
  return value
}

function u64 (value, field) {
  if (typeof value === 'number') {
    if (!Number.isSafeInteger(value) || value < 0) fail('BAD_CLIENT_INPUT', `${field} must be an unsigned safe integer or bigint`)
    value = BigInt(value)
  }
  if (typeof value !== 'bigint' || value < 0n || value > ((1n << 64n) - 1n)) fail('BAD_CLIENT_INPUT', `${field} is outside u64`)
  return value
}

function nonce (runtime, value) {
  return value == null ? randomBytes(runtime, 32, 'client nonce') : b4a.from(asBytes(value, 'clientNonce', 32))
}

async function admission (options, context, required) {
  return resolveAdmission(options, context, required,
    'this operation requires an admission provider or admission value')
}

function result (encoding, request, requestCommitment, familyId, operationId, expectedResultBodyBytes = null) {
  const profile = selectedOperationProfile(familyId, operationId)
  return {
    request,
    requestBytes: encodeCanonical(encoding, request),
    requestCommitment,
    wire: Object.freeze({
      familyId,
      operationId,
      expectedResultBodyBytes: expectedResultBodyBytes == null ? profile.maxResultBodyBytes : expectedResultBodyBytes
    })
  }
}

export async function createCellReplica (options) {
  if (!options || typeof options !== 'object') fail('BAD_CLIENT_INPUT', 'cell replica options are required')
  const runtime = options.runtime
  const relayPublicKey = asBytes(options.relayPublicKey, 'relayPublicKey', 32)
  const allocationEpoch = u32(options.allocationEpoch, 'allocationEpoch')
  const sizeClass = options.sizeClass
  const leaseClass = options.leaseClass
  const capabilities = generateDistinctCapabilityKeys(runtime, ['create', 'renew', 'drop'], [relayPublicKey])
  const storageSlot = cellStorageSlot({ allocationEpoch, createPublicKey: capabilities.create.publicKey })
  let sealed
  let writeCap
  try {
    sealed = await sealCell({
      runtime,
      storageSlot,
      sizeClass,
      structuredContent: options.structuredContent
    })
    const declaredBlobHash = blake2b256(sealed.cellBlob)
    const allocation = allocationCommitment({
      relayPublicKey,
      storageSlot,
      allocationEpoch,
      sizeClass,
      leaseClass,
      declaredCellBlobHash: declaredBlobHash,
      createPublicKey: capabilities.create.publicKey,
      renewPublicKey: capabilities.renew.publicKey,
      dropPublicKey: capabilities.drop.publicKey
    })
    const clientNonce = nonce(runtime, options.clientNonce)
    const requestCommitment = cellPutRequestCommitment({ allocationCommitment: allocation, clientNonce })
    const admissionValue = await admission(options, {
      familyId: FAMILY.CELL,
      operationId: OPERATION.CELL.PUT,
      requestCommitment,
      relayPublicKey,
      sizeClass,
      leaseClass
    }, true)
    const request = {
      version: 1,
      storageSlot,
      allocationEpoch,
      sizeClass,
      leaseClass,
      clientNonce,
      createPublicKey: capabilities.create.publicKey,
      renewPublicKey: capabilities.renew.publicKey,
      dropPublicKey: capabilities.drop.publicKey,
      declaredBlobHash,
      createSignature: signCapability(capabilities.create.privateSeed, allocation),
      admission: admissionValue,
      cellBlob: sealed.cellBlob
    }
    const readCap = {
      version: 1,
      relayPublicKey: b4a.from(relayPublicKey),
      storageSlot: b4a.from(storageSlot),
      cellKey: sealed.cellKey,
      sizeClass,
      expectedCellBlobHash: b4a.from(declaredBlobHash)
    }
    writeCap = {
      readCap,
      allocationEpoch,
      createPrivateKey: capabilities.create.privateSeed,
      renewPrivateKey: capabilities.renew.privateSeed,
      dropPrivateKey: capabilities.drop.privateSeed
    }
    return {
      ...result(putCellV1, request, requestCommitment, FAMILY.CELL, OPERATION.CELL.PUT),
      allocationCommitment: allocation,
      readCap,
      writeCap
    }
  } catch (error) {
    if (writeCap) destroyCellCapabilities(writeCap)
    else {
      wipe(capabilities.create.privateSeed)
      wipe(capabilities.renew.privateSeed)
      wipe(capabilities.drop.privateSeed)
      if (sealed) wipe(sealed.cellKey)
    }
    throw error
  }
}

function writeFields (options) {
  if (!options || typeof options !== 'object' || !options.writeCap || !options.writeCap.readCap) {
    fail('BAD_CLIENT_INPUT', 'writeCap with readCap is required')
  }
  const readCap = options.writeCap.readCap
  return {
    relayPublicKey: asBytes(readCap.relayPublicKey, 'relayPublicKey', 32),
    storageSlot: asBytes(readCap.storageSlot, 'storageSlot', 32)
  }
}

export async function createRenewCellRequest (options) {
  const runtime = options && options.runtime
  const fields = writeFields(options)
  const clientNonce = nonce(runtime, options.clientNonce)
  const expectedRevision = u64(options.expectedRevision, 'expectedRevision')
  const expectedLeaseEpoch = u32(options.expectedLeaseEpoch, 'expectedLeaseEpoch')
  const leaseClass = options.leaseClass
  const requestCommitment = cellManageRequestCommitment({
    operation: 'cell-renew',
    ...fields,
    expectedRevision,
    expectedLeaseEpoch,
    requestedLeaseClass: leaseClass,
    clientNonce
  })
  const admissionValue = await admission(options, {
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.RENEW,
    requestCommitment,
    relayPublicKey: fields.relayPublicKey,
    leaseClass
  }, true)
  const request = {
    version: 1,
    storageSlot: fields.storageSlot,
    expectedRevision,
    expectedLeaseEpoch,
    leaseClass,
    clientNonce,
    admission: admissionValue,
    signature: signCapability(options.writeCap.renewPrivateKey, requestCommitment)
  }
  return result(renewCellV1, request, requestCommitment, FAMILY.CELL, OPERATION.CELL.RENEW)
}

export function createDropCellRequest (options) {
  const runtime = options && options.runtime
  const fields = writeFields(options)
  const clientNonce = nonce(runtime, options.clientNonce)
  const expectedRevision = u64(options.expectedRevision, 'expectedRevision')
  const expectedLeaseEpoch = u32(options.expectedLeaseEpoch, 'expectedLeaseEpoch')
  const requestCommitment = cellManageRequestCommitment({
    operation: 'cell-drop',
    ...fields,
    expectedRevision,
    expectedLeaseEpoch,
    requestedLeaseClass: 0,
    clientNonce
  })
  const request = {
    version: 1,
    storageSlot: fields.storageSlot,
    expectedRevision,
    expectedLeaseEpoch,
    clientNonce,
    signature: signCapability(options.writeCap.dropPrivateKey, requestCommitment)
  }
  return result(dropCellV1, request, requestCommitment, FAMILY.CELL, OPERATION.CELL.DROP)
}

async function createReadRequest (options, operationId, commitment, encoding) {
  if (!options || typeof options !== 'object' || !options.readCap) fail('BAD_CLIENT_INPUT', 'readCap is required')
  const runtime = options.runtime
  const relayPublicKey = asBytes(options.readCap.relayPublicKey, 'relayPublicKey', 32)
  const storageSlot = asBytes(options.readCap.storageSlot, 'storageSlot', 32)
  const clientNonce = nonce(runtime, options.clientNonce)
  const requestCommitment = commitment({ relayPublicKey, storageSlot, clientNonce })
  const admissionValue = await admission(options, {
    familyId: FAMILY.CELL,
    operationId,
    requestCommitment,
    relayPublicKey
  }, false)
  const request = { version: 1, storageSlot, clientNonce, admission: admissionValue }
  let expectedResultBodyBytes = null
  if (operationId === OPERATION.CELL.GET) {
    const bytes = CELL_SIZE_CLASS[options.readCap.sizeClass]
    if (bytes == null) fail('BAD_CLIENT_INPUT', 'readCap sizeClass is outside the frozen cell classes')
    expectedResultBodyBytes = 2 + bytes
  }
  return result(encoding, request, requestCommitment, FAMILY.CELL, operationId, expectedResultBodyBytes)
}

export const createGetCellRequest = options => createReadRequest(options, OPERATION.CELL.GET, cellGetRequestCommitment, getCellV1)
export const createProveCellRequest = options => createReadRequest(options, OPERATION.CELL.PROVE, cellProveRequestCommitment, proveCellV1)

export async function createBatchGetRequest (options) {
  if (!options || typeof options !== 'object') fail('BAD_CLIENT_INPUT', 'batch options are required')
  const runtime = options.runtime
  const relayPublicKey = asBytes(options.relayPublicKey, 'relayPublicKey', 32)
  if (!Array.isArray(options.slots)) fail('BAD_CLIENT_INPUT', 'slots must be an array')
  const slots = options.slots.map((slot, index) => b4a.from(asBytes(slot, `slots[${index}]`, 32)))
  const clientNonce = nonce(runtime, options.clientNonce)
  const requestCommitment = cellBatchGetRequestCommitment({ relayPublicKey, slots, clientNonce })
  const admissionValue = await admission(options, {
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.BATCH_GET,
    requestCommitment,
    relayPublicKey,
    count: slots.length
  }, false)
  const request = { version: 1, clientNonce, slots, admission: admissionValue }
  return result(batchGetV1, request, requestCommitment, FAMILY.CELL, OPERATION.CELL.BATCH_GET)
}