OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-daemon / admission-coordinator.js

711 lines · javascript ref:
import b4a from 'b4a'
import {
  ADMISSION_MODE,
  RESULT_SIGNATURE_DOMAIN_ID,
  admissionParametersHash,
  admissionParametersV1,
  decodeCanonical,
  encodeCanonical,
  resultSignaturePayload
} from '@hiverelay/blind-protocol'
import { DESCRIPTOR_STATE_KIND } from './descriptor-state.js'

const MAX_U64 = (1n << 64n) - 1n
const MAX_WAL_COMMIT_RECORD_BYTES = 16 * 1024
const PREFLIGHT_AUTHORITIES = new WeakMap()

// The production daemon assembly injects the server-owned PostEOF issuer's
// consumer. Individual coordinators still fail closed when no consumer is
// supplied; a caller assertion or public-field object is never EOF authority.
export const ADMISSION_PREFLIGHT_SPLIT_STATUS = Object.freeze({
  wired: true,
  daemonPrivatePostEofBrandRequired: true,
  postEofAuthorityRequired: true,
  productionReady: false,
  blocker: 'PRODUCTION_ADMISSION_ADAPTER_CAPTURE_REQUIRED'
})

function protocolFailure (code, message) {
  const error = new Error(message)
  error.code = code
  throw error
}

function abortFailure () {
  const error = new Error('admission preflight crossed its abort fence')
  error.code = 'ABORT_ERR'
  return error
}

function assertLiveSignal (signal) {
  if (signal == null) return
  if (typeof signal !== 'object' || typeof signal.aborted !== 'boolean' ||
      typeof signal.addEventListener !== 'function' || typeof signal.removeEventListener !== 'function') {
    protocolFailure('SPEND_INVALID', 'admission preflight signal must be an AbortSignal')
  }
  if (signal.aborted) throw abortFailure()
}

async function abortableCall (operation, signal) {
  assertLiveSignal(signal)
  if (signal == null) return operation()
  let onAbort
  const aborted = new Promise((resolve, reject) => {
    onAbort = () => reject(abortFailure())
    signal.addEventListener('abort', onAbort, { once: true })
  })
  try {
    return await Promise.race([Promise.resolve().then(operation), aborted])
  } finally {
    signal.removeEventListener('abort', onAbort)
  }
}

function asBytes (value, field, limits = null) {
  if (!value || typeof value.byteLength !== 'number') protocolFailure('SPEND_INVALID', `${field} must be bytes`)
  const bytes = b4a.isBuffer(value)
    ? value
    : ArrayBuffer.isView(value)
      ? b4a.from(value.buffer, value.byteOffset, value.byteLength)
      : b4a.from(value)
  if (limits && (bytes.byteLength < limits[0] || bytes.byteLength > limits[1])) {
    protocolFailure('SPEND_INVALID', `${field} is outside its byte bound`)
  }
  return bytes
}

function sameBytes (left, right) {
  return Boolean(left && right && left.byteLength === right.byteLength && b4a.equals(left, right))
}

function nonzero (bytes) {
  for (const byte of bytes) if (byte !== 0) return true
  return false
}

function byteKey (bytes) {
  return b4a.toString(bytes, 'hex')
}

function epoch (descriptorState) {
  const value = descriptorState.epochNow()
  if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) {
    protocolFailure('SPEND_INVALID', 'descriptor epoch is outside u32')
  }
  return value
}

function u64 (value, field) {
  if (typeof value === 'number') value = BigInt(value)
  if (typeof value !== 'bigint' || value < 0n || value > MAX_U64) {
    protocolFailure('SPEND_INVALID', `${field} is outside u64`)
  }
  return value
}

function decodeExact (canonicalBytes) {
  let value
  try {
    value = decodeCanonical(admissionParametersV1, canonicalBytes, { copyBytes: true })
    if (!sameBytes(canonicalBytes, encodeCanonical(admissionParametersV1, value))) throw new Error('non-canonical')
  } catch {
    protocolFailure('SPEND_INVALID', 'admission parameters are not canonical')
  }
  return value
}

function profileCopy (profile) {
  return Object.freeze({
    ...profile,
    parameterUrl: profile.parameterUrl == null ? null : b4a.from(profile.parameterUrl),
    parameterHash: b4a.from(profile.parameterHash)
  })
}

function recordView (record) {
  return Object.freeze({
    value: decodeExact(record.canonicalBytes),
    hash: b4a.from(record.hash),
    canonicalBytes: b4a.from(record.canonicalBytes)
  })
}

function currentSnapshot (descriptorState, candidate = null) {
  if (candidate != null) {
    if (candidate.hash == null) protocolFailure('SPEND_INVALID', 'descriptor snapshot has no signed hash')
    const selected = descriptorState.selected(candidate.hash)
    if (selected != null && descriptorState.remainsUsable(selected)) return selected
    protocolFailure('SPEND_INVALID', 'descriptor snapshot is no longer retained or usable')
  }
  const state = descriptorState.state()
  if (state.kind !== DESCRIPTOR_STATE_KIND.READY) protocolFailure('SPEND_INVALID', 'descriptor is not current')
  return state.snapshot
}

function parameterMatchesProfile (record, profile, descriptor) {
  const value = decodeExact(record.canonicalBytes)
  return value.profileId === profile.profileId && value.schemeId === profile.schemeId &&
    value.conformanceClass === profile.conformanceClass && value.roleBits === profile.roleBits &&
    sameBytes(record.hash, profile.parameterHash) && sameBytes(value.relayPublicKey, descriptor.relayPublicKey) &&
    value.validFromEpoch < descriptor.expiresEpoch && value.expiresEpoch > descriptor.issuedEpoch
}

function admissionCopy (input) {
  if (!input || typeof input !== 'object') protocolFailure('SPEND_REQUIRED', 'operation requires admission')
  return Object.freeze({
    profileId: input.profileId,
    schemeId: input.schemeId,
    parameterHash: b4a.from(asBytes(input.parameterHash, 'parameterHash', [32, 32])),
    token: b4a.from(asBytes(input.token, 'token', [1, 65535]))
  })
}

function operationBinding (profile) {
  if (!profile || !Number.isInteger(profile.familyId) || !Number.isInteger(profile.operationId) ||
      !Number.isInteger(profile.admissionMode) || profile.admissionMode === ADMISSION_MODE.NONE) {
    protocolFailure('SPEND_INVALID', 'admission preflight requires one admitted operation profile')
  }
  return Object.freeze({
    familyId: profile.familyId,
    operationId: profile.operationId,
    admissionMode: profile.admissionMode
  })
}

function costBinding (cost) {
  if (!cost || !Number.isInteger(cost.resourceClass) || !Number.isInteger(cost.leaseClass)) {
    protocolFailure('SPEND_INVALID', 'admission cost class is unavailable')
  }
  return Object.freeze({ resourceClass: cost.resourceClass, leaseClass: cost.leaseClass })
}

function sameOperationBinding (left, right) {
  return left.familyId === right.familyId && left.operationId === right.operationId &&
    left.admissionMode === right.admissionMode
}

function sameAdmissionBinding (left, right) {
  return left.profileId === right.profileId && left.schemeId === right.schemeId &&
    sameBytes(left.parameterHash, right.parameterHash) && sameBytes(left.token, right.token)
}

function sameCostBinding (left, right) {
  return left.resourceClass === right.resourceClass && left.leaseClass === right.leaseClass &&
    left.costUnits === right.costUnits
}

function opaqueFrozenCapability (value) {
  try {
    return Boolean(value && typeof value === 'object' && Object.isFrozen(value) &&
      Reflect.ownKeys(value).length === 0)
  } catch {
    return false
  }
}

export class AdmissionCoordinator {
  #recordsByHash
  #parameterBytes
  #serial
  #consumePostEofAuthority

  constructor (options = {}) {
    if (!options.descriptorState || typeof options.descriptorState.state !== 'function' ||
        typeof options.descriptorState.selected !== 'function' ||
        typeof options.descriptorState.remainsUsable !== 'function') {
      throw new TypeError('descriptorState is required')
    }
    if (typeof options.verifySignature !== 'function') {
      throw new TypeError('verifySignature is required for admission parameters')
    }
    if (typeof options.resolveAdapter !== 'function') {
      throw new TypeError('resolveAdapter is required for admission redemption')
    }
    this.descriptorState = options.descriptorState
    this.verifySignature = options.verifySignature
    this.resolveAdapter = options.resolveAdapter
    if (options.consumePostEofAuthority != null && typeof options.consumePostEofAuthority !== 'function') {
      throw new TypeError('consumePostEofAuthority must be a function')
    }
    this.#consumePostEofAuthority = options.consumePostEofAuthority || null
    this.maxRecords = options.maxRecords == null ? 64 : options.maxRecords
    this.maxParameterBytes = options.maxParameterBytes == null ? 1024 * 1024 : options.maxParameterBytes
    if (!Number.isSafeInteger(this.maxRecords) || this.maxRecords < 1 || this.maxRecords > 4096) {
      throw new TypeError('maxRecords must be within 1..4096')
    }
    if (!Number.isSafeInteger(this.maxParameterBytes) || this.maxParameterBytes < 16 * 1024 ||
        this.maxParameterBytes > 64 * 1024 * 1024) {
      throw new TypeError('maxParameterBytes must be within 16KiB..64MiB')
    }
    this.#recordsByHash = new Map()
    this.#parameterBytes = 0
    this.#serial = Promise.resolve()
  }

  _serialized (operation) {
    const result = this.#serial.then(operation)
    this.#serial = result.catch(() => {})
    return result
  }

  installParameters (input, options = {}) {
    const canonicalBytes = b4a.from(asBytes(input, 'admission parameter bytes'))
    return this._serialized(() => this._installParameters(canonicalBytes, options.signal))
  }

  async _installParameters (canonicalBytes, signal) {
    let value = decodeExact(canonicalBytes)
    const hash = admissionParametersHash(canonicalBytes)
    const unsignedBytes = canonicalBytes.subarray(0, canonicalBytes.byteLength - 64)
    const payload = resultSignaturePayload(RESULT_SIGNATURE_DOMAIN_ID.ADMISSION_PARAMETERS, unsignedBytes)
    const verified = await this.verifySignature({
      domainId: RESULT_SIGNATURE_DOMAIN_ID.ADMISSION_PARAMETERS,
      publicKey: b4a.from(value.relayPublicKey),
      signature: b4a.from(value.signature),
      payload: b4a.from(payload),
      canonicalBytes: b4a.from(canonicalBytes),
      parameters: decodeExact(canonicalBytes),
      signal
    })
    if (verified !== true) protocolFailure('SPEND_INVALID', 'admission parameter signature verification failed')
    value = decodeExact(canonicalBytes)

    const key = byteKey(hash)
    const existing = this.#recordsByHash.get(key)
    if (existing) {
      if (!sameBytes(existing.canonicalBytes, canonicalBytes)) {
        protocolFailure('SPEND_INVALID', 'admission parameter hash was rebound to different bytes')
      }
      return recordView(existing)
    }
    this._sweep(epoch(this.descriptorState))
    if (this.#recordsByHash.size >= this.maxRecords ||
        this.#parameterBytes + canonicalBytes.byteLength > this.maxParameterBytes) {
      protocolFailure('BUSY', 'bounded admission-parameter storage is full')
    }
    const record = Object.freeze({
      hash: b4a.from(hash),
      canonicalBytes: b4a.from(canonicalBytes),
      expiresEpoch: value.expiresEpoch
    })
    this.#recordsByHash.set(key, record)
    this.#parameterBytes += canonicalBytes.byteLength
    return recordView(record)
  }

  _sweep (now) {
    for (const [key, record] of this.#recordsByHash) {
      if (record.expiresEpoch <= now) {
        this.#recordsByHash.delete(key)
        this.#parameterBytes -= record.canonicalBytes.byteLength
      }
    }
  }

  profileRecord (descriptorSnapshot, profileId, schemeId, options = {}) {
    if (!descriptorSnapshot || !descriptorSnapshot.hash) return null
    const selectedSnapshot = this.descriptorState.selected(descriptorSnapshot.hash)
    if (!selectedSnapshot) return null
    const descriptor = selectedSnapshot.descriptor
    const profile = descriptor.admissionProfiles.find(entry => entry.profileId === profileId)
    if (!profile || profile.schemeId !== schemeId) return null
    const record = this.#recordsByHash.get(byteKey(profile.parameterHash))
    if (!record || !parameterMatchesProfile(record, profile, descriptor)) return null
    const value = decodeExact(record.canonicalBytes)
    const now = epoch(this.descriptorState)
    const available = value.expiresEpoch > now
    const redeemable = available && value.validFromEpoch <= now
    if (!available || (options.redeemable === true && !redeemable)) return null
    return Object.freeze({
      profile: profileCopy(profile),
      record: recordView(record),
      redeemable
    })
  }

  descriptorParametersAvailable (descriptorSnapshot = null) {
    try {
      descriptorSnapshot = currentSnapshot(this.descriptorState, descriptorSnapshot)
    } catch {
      return false
    }
    return descriptorSnapshot.descriptor.admissionProfiles.every(profile =>
      this.profileRecord(descriptorSnapshot, profile.profileId, profile.schemeId) != null)
  }

  descriptorProfilesReady (descriptorSnapshot = null) {
    try {
      descriptorSnapshot = currentSnapshot(this.descriptorState, descriptorSnapshot)
    } catch {
      return false
    }
    return descriptorSnapshot.descriptor.admissionProfiles.every(profile =>
      this.profileRecord(descriptorSnapshot, profile.profileId, profile.schemeId, { redeemable: true }) != null)
  }

  parametersForRequest (request, descriptorSnapshot = null) {
    descriptorSnapshot = currentSnapshot(this.descriptorState, descriptorSnapshot)
    const selected = this.profileRecord(descriptorSnapshot, request.profileId, request.schemeId)
    return selected == null ? null : b4a.from(selected.record.canonicalBytes)
  }

  _preflightBinding (input) {
    if (!input || typeof input !== 'object') protocolFailure('SPEND_INVALID', 'admission preflight input is required')
    const operation = operationBinding(input.profile)
    const admission = admissionCopy(input.admission)
    const requestCommitment = b4a.from(asBytes(input.requestCommitment,
      'requestCommitment', [32, 32]))
    const requestId = b4a.from(asBytes(input.requestId, 'requestId', [16, 16]))
    const requestedCost = costBinding(input.cost)
    const descriptorSnapshot = currentSnapshot(this.descriptorState, input.descriptorSnapshot)
    const current = currentSnapshot(this.descriptorState)
    if (descriptorSnapshot.descriptorSequence !== current.descriptorSequence ||
        !sameBytes(descriptorSnapshot.hash, current.hash)) {
      protocolFailure('SPEND_INVALID', 'admission preflight requires the exact current descriptor')
    }
    const selected = this.profileRecord(descriptorSnapshot, admission.profileId, admission.schemeId,
      { redeemable: true })
    if (!selected || !sameBytes(admission.parameterHash, selected.profile.parameterHash)) {
      protocolFailure('SPEND_INVALID', 'admission profile, scheme, or parameter hash is not current')
    }
    const endpoint = descriptorSnapshot.descriptor.endpoints.find(entry =>
      input.endpoint && entry.endpointId === input.endpoint.endpointId)
    if (!endpoint || input.endpoint.roleBits !== endpoint.roleBits ||
        (selected.record.value.roleBits & endpoint.roleBits) === 0) {
      protocolFailure('SPEND_INVALID', 'admission profile is not bound to the exact selected endpoint')
    }
    const parameters = selected.record.value
    const now = epoch(this.descriptorState)
    if (now < parameters.validFromEpoch || now >= parameters.expiresEpoch ||
        admission.token.byteLength > parameters.tokenMaxBytes) {
      protocolFailure('SPEND_INVALID', 'admission parameters are not redeemable at the current epoch')
    }
    const row = parameters.resourceCosts.find(entry => entry.familyId === operation.familyId &&
      entry.operationId === operation.operationId && entry.resourceClass === requestedCost.resourceClass &&
      entry.leaseClass === requestedCost.leaseClass)
    if (!row) protocolFailure('SPEND_INVALID', 'admission parameters contain no exact resource-cost tuple')
    return Object.freeze({
      operation,
      admission,
      requestId,
      requestCommitment,
      descriptorSequence: descriptorSnapshot.descriptorSequence,
      descriptorHash: b4a.from(descriptorSnapshot.hash),
      endpointId: endpoint.endpointId,
      endpointRoleBits: endpoint.roleBits,
      cost: Object.freeze({
        resourceClass: requestedCost.resourceClass,
        leaseClass: requestedCost.leaseClass,
        costUnits: row.costUnits
      }),
      parameterCanonicalBytes: b4a.from(selected.record.canonicalBytes)
    })
  }

  _revalidatePreflight (binding) {
    const descriptorSnapshot = currentSnapshot(this.descriptorState)
    if (descriptorSnapshot.descriptorSequence !== binding.descriptorSequence ||
        !sameBytes(descriptorSnapshot.hash, binding.descriptorHash) ||
        !this.descriptorState.remainsUsable(descriptorSnapshot)) {
      protocolFailure('SPEND_INVALID', 'admission preflight descriptor advanced across its fence')
    }
    const selected = this.profileRecord(descriptorSnapshot,
      binding.admission.profileId, binding.admission.schemeId, { redeemable: true })
    if (!selected || !sameBytes(selected.profile.parameterHash, binding.admission.parameterHash) ||
        !sameBytes(selected.record.canonicalBytes, binding.parameterCanonicalBytes)) {
      protocolFailure('SPEND_INVALID', 'admission preflight parameter authority changed')
    }
    const endpoint = descriptorSnapshot.descriptor.endpoints.find(entry => entry.endpointId === binding.endpointId)
    if (!endpoint || endpoint.roleBits !== binding.endpointRoleBits ||
        (selected.record.value.roleBits & endpoint.roleBits) === 0) {
      protocolFailure('SPEND_INVALID', 'admission preflight endpoint authority changed')
    }
    const parameters = selected.record.value
    const now = epoch(this.descriptorState)
    if (now < parameters.validFromEpoch || now >= parameters.expiresEpoch ||
        binding.admission.token.byteLength > parameters.tokenMaxBytes) {
      protocolFailure('SPEND_INVALID', 'admission preflight expired before confirmation')
    }
    const row = parameters.resourceCosts.find(entry => entry.familyId === binding.operation.familyId &&
      entry.operationId === binding.operation.operationId &&
      entry.resourceClass === binding.cost.resourceClass && entry.leaseClass === binding.cost.leaseClass)
    if (!row || row.costUnits !== binding.cost.costUnits) {
      protocolFailure('SPEND_INVALID', 'admission preflight cost authority changed')
    }
    return { descriptorSnapshot, endpoint, parameters }
  }

  _adapterPreflightInput (binding, signal, adapterPreflight = null) {
    // mutationAllowed is a contract marker, not a sandbox. Both split adapter
    // methods may validate and prepare owned proof bytes only; they must not
    // contact an issuer, consume a spend, append WAL, publish, or mutate state.
    return {
      admission: {
        profileId: binding.admission.profileId,
        schemeId: binding.admission.schemeId,
        parameterHash: b4a.from(binding.admission.parameterHash),
        token: b4a.from(binding.admission.token)
      },
      familyId: binding.operation.familyId,
      operationId: binding.operation.operationId,
      costClass: Object.freeze({
        resourceClass: binding.cost.resourceClass,
        leaseClass: binding.cost.leaseClass,
        costUnits: binding.cost.costUnits
      }),
      requestCommitment: b4a.from(binding.requestCommitment),
      parameters: decodeExact(binding.parameterCanonicalBytes),
      endpointId: binding.endpointId,
      endpointRoleBits: binding.endpointRoleBits,
      descriptorSequence: binding.descriptorSequence,
      descriptorHash: b4a.from(binding.descriptorHash),
      mutationAllowed: false,
      adapterPreflight,
      signal
    }
  }

  async preparePreflight (input) {
    const signal = input && input.signal
    assertLiveSignal(signal)
    const binding = this._preflightBinding(input)
    if (this.#consumePostEofAuthority == null) {
      protocolFailure('SPEND_INVALID', 'admission preflight is disabled without a post-EOF authority consumer')
    }
    const live = this._revalidatePreflight(binding)
    const adapter = await abortableCall(() => this.resolveAdapter({
      profileId: binding.admission.profileId,
      schemeId: binding.admission.schemeId,
      parameterHash: b4a.from(binding.admission.parameterHash),
      descriptor: live.descriptorSnapshot.descriptor,
      parameters: decodeExact(binding.parameterCanonicalBytes),
      endpointId: binding.endpointId,
      endpointRoleBits: binding.endpointRoleBits,
      signal
    }), signal)
    const adapterReceiver = adapter
    const preparePreflight = adapterReceiver == null ? null : adapterReceiver.preparePreflight
    const confirmAfterEof = adapterReceiver == null ? null : adapterReceiver.confirmAfterEof
    if (typeof preparePreflight !== 'function' || typeof confirmAfterEof !== 'function') {
      protocolFailure('SPEND_INVALID', 'admission adapter has no explicit side-effect-free preflight and confirmation split')
    }
    const adapterPreflight = await abortableCall(() => preparePreflight.call(
      adapterReceiver, this._adapterPreflightInput(binding, signal)), signal)
    if (!opaqueFrozenCapability(adapterPreflight)) {
      protocolFailure('SPEND_INVALID', 'admission adapter returned no empty frozen opaque preflight capability')
    }
    this._revalidatePreflight(binding)
    assertLiveSignal(signal)
    const authority = Object.freeze({})
    PREFLIGHT_AUTHORITIES.set(authority, Object.freeze({
      owner: this,
      binding,
      adapterReceiver,
      confirmAfterEof,
      adapterPreflight
    }))
    return authority
  }

  async confirmAfterEof (authority, input) {
    const retained = authority && PREFLIGHT_AUTHORITIES.get(authority)
    if (!retained || retained.owner !== this) {
      protocolFailure('SPEND_INVALID', 'admission confirmation requires one live branded preflight authority')
    }
    // Every confirmation attempt burns the authority before validating any
    // caller echo or invoking an adapter. Failed substitutions cannot retry it.
    PREFLIGHT_AUTHORITIES.delete(authority)
    const signal = input && input.signal
    assertLiveSignal(signal)
    const echoed = this._preflightBinding(input)
    const binding = retained.binding
    if (!sameOperationBinding(echoed.operation, binding.operation) ||
        !sameAdmissionBinding(echoed.admission, binding.admission) ||
        !sameBytes(echoed.requestId, binding.requestId) ||
        !sameBytes(echoed.requestCommitment, binding.requestCommitment) ||
        echoed.descriptorSequence !== binding.descriptorSequence ||
        !sameBytes(echoed.descriptorHash, binding.descriptorHash) ||
        echoed.endpointId !== binding.endpointId || echoed.endpointRoleBits !== binding.endpointRoleBits ||
        !sameCostBinding(echoed.cost, binding.cost) ||
        !sameBytes(echoed.parameterCanonicalBytes, binding.parameterCanonicalBytes)) {
      protocolFailure('SPEND_INVALID', 'admission confirmation changed its preflight binding')
    }
    this._revalidatePreflight(binding)
    if (this.#consumePostEofAuthority == null) {
      protocolFailure('SPEND_INVALID', 'admission confirmation is disabled without a post-EOF authority consumer')
    }
    if (!opaqueFrozenCapability(input.postEofAuthority)) {
      protocolFailure('SPEND_INVALID', 'post-EOF authority must be an empty frozen daemon-private capability')
    }
    const postEofConsumed = await abortableCall(() => this.#consumePostEofAuthority({
      authority: input.postEofAuthority,
      descriptorSequence: binding.descriptorSequence,
      descriptorHash: b4a.from(binding.descriptorHash),
      endpointId: binding.endpointId,
      familyId: binding.operation.familyId,
      operationId: binding.operation.operationId,
      requestId: b4a.from(binding.requestId),
      requestCommitment: b4a.from(binding.requestCommitment),
      signal
    }), signal)
    if (postEofConsumed !== true) {
      protocolFailure('SPEND_INVALID', 'admission confirmation has no exact consumed post-EOF authority')
    }
    this._revalidatePreflight(binding)
    const prepared = await abortableCall(() => retained.confirmAfterEof.call(
      retained.adapterReceiver,
      this._adapterPreflightInput(binding, signal, retained.adapterPreflight)), signal)
    this._revalidatePreflight(binding)
    assertLiveSignal(signal)
    if (!prepared || typeof prepared !== 'object') {
      protocolFailure('SPEND_INVALID', 'admission adapter returned no confirmed proof')
    }
    const spendTag = asBytes(prepared.spendTag, 'spendTag', [32, 32])
    const requestCommitment = asBytes(prepared.requestCommitment,
      'prepared requestCommitment', [32, 32])
    const parameterHash = asBytes(prepared.parameterHash, 'prepared parameterHash', [32, 32])
    const walCommitRecord = asBytes(prepared.walCommitRecord,
      'walCommitRecord', [1, MAX_WAL_COMMIT_RECORD_BYTES])
    const preparedCost = prepared.costClass
    if (!nonzero(spendTag) || !sameBytes(requestCommitment, binding.requestCommitment) ||
        prepared.profileId !== binding.admission.profileId || prepared.schemeId !== binding.admission.schemeId ||
        !sameBytes(parameterHash, binding.admission.parameterHash) || !preparedCost ||
        preparedCost.resourceClass !== binding.cost.resourceClass ||
        preparedCost.leaseClass !== binding.cost.leaseClass ||
        u64(preparedCost.costUnits, 'prepared costUnits') !== binding.cost.costUnits) {
      protocolFailure('SPEND_INVALID', 'confirmed admission proof changed its exact preflight binding and cost')
    }
    return Object.freeze({
      spendTag: b4a.from(spendTag),
      requestCommitment: b4a.from(requestCommitment),
      costClass: Object.freeze({
        resourceClass: binding.cost.resourceClass,
        leaseClass: binding.cost.leaseClass,
        costUnits: binding.cost.costUnits
      }),
      walCommitRecord: b4a.from(walCommitRecord),
      profileId: binding.admission.profileId,
      schemeId: binding.admission.schemeId,
      parameterHash: b4a.from(binding.admission.parameterHash)
    })
  }

  async prepare (input) {
    const { profile, signal } = input
    const admission = input.admission == null
      ? null
      : Object.freeze({
        profileId: input.admission.profileId,
        schemeId: input.admission.schemeId,
        parameterHash: b4a.from(asBytes(input.admission.parameterHash, 'parameterHash', [32, 32])),
        token: b4a.from(asBytes(input.admission.token, 'token', [1, 65535]))
      })
    if (profile.admissionMode === ADMISSION_MODE.NONE) {
      if (admission != null) protocolFailure('BAD_ENCODING', 'operation forbids admission')
      return null
    }
    if (admission == null) {
      if (profile.admissionMode === ADMISSION_MODE.REQUIRED) {
        protocolFailure('SPEND_REQUIRED', 'operation requires admission')
      }
      return null
    }
    const requestCommitment = b4a.from(asBytes(input.requestCommitment,
      'requestCommitment', [32, 32]))
    const descriptorSnapshot = currentSnapshot(this.descriptorState, input.descriptorSnapshot)
    const selected = this.profileRecord(descriptorSnapshot, admission.profileId, admission.schemeId,
      { redeemable: true })
    if (!selected || !sameBytes(admission.parameterHash, selected.profile.parameterHash)) {
      protocolFailure('SPEND_INVALID', 'admission profile, scheme, or parameter hash is not current')
    }
    const endpoint = descriptorSnapshot.descriptor.endpoints.find(entry => entry.endpointId === input.endpoint.endpointId)
    if (!endpoint || (selected.record.value.roleBits & endpoint.roleBits) === 0) {
      protocolFailure('SPEND_INVALID', 'admission profile is not bound to the selected endpoint role')
    }
    const cost = input.cost
    if (!cost || !Number.isInteger(cost.resourceClass) || !Number.isInteger(cost.leaseClass)) {
      protocolFailure('SPEND_INVALID', 'admission cost class is unavailable')
    }
    let parameters = selected.record.value
    const now = epoch(this.descriptorState)
    if (now < parameters.validFromEpoch || now >= parameters.expiresEpoch ||
        admission.token.byteLength > parameters.tokenMaxBytes) {
      protocolFailure('SPEND_INVALID', 'admission parameters are not redeemable at the current epoch')
    }
    let row = parameters.resourceCosts.find(entry => entry.familyId === profile.familyId &&
      entry.operationId === profile.operationId && entry.resourceClass === cost.resourceClass &&
      entry.leaseClass === cost.leaseClass)
    if (!row) protocolFailure('SPEND_INVALID', 'admission parameters contain no exact resource-cost tuple')

    const adapter = await this.resolveAdapter({
      profileId: admission.profileId,
      schemeId: admission.schemeId,
      parameterHash: b4a.from(admission.parameterHash),
      descriptor: currentSnapshot(this.descriptorState, descriptorSnapshot).descriptor,
      parameters: decodeExact(selected.record.canonicalBytes),
      endpointId: endpoint.endpointId,
      endpointRoleBits: endpoint.roleBits,
      signal
    })
    if (!adapter || typeof adapter.prepare !== 'function' ||
        !this.descriptorState.remainsUsable(descriptorSnapshot)) {
      protocolFailure('SPEND_INVALID', 'admission verifier is unavailable or descriptor advanced across a fence')
    }
    parameters = decodeExact(selected.record.canonicalBytes)
    row = parameters.resourceCosts.find(entry => entry.familyId === profile.familyId &&
      entry.operationId === profile.operationId && entry.resourceClass === cost.resourceClass &&
      entry.leaseClass === cost.leaseClass)
    if (!row) protocolFailure('SPEND_INVALID', 'admission cost tuple changed during verification')

    const prepared = await adapter.prepare({
      admission: {
        profileId: admission.profileId,
        schemeId: admission.schemeId,
        parameterHash: b4a.from(admission.parameterHash),
        token: b4a.from(admission.token)
      },
      familyId: profile.familyId,
      operationId: profile.operationId,
      costClass: Object.freeze({
        resourceClass: cost.resourceClass,
        leaseClass: cost.leaseClass,
        costUnits: row.costUnits
      }),
      requestCommitment: b4a.from(requestCommitment),
      parameters: decodeExact(selected.record.canonicalBytes),
      endpointId: endpoint.endpointId,
      signal
    })
    if (!prepared || typeof prepared !== 'object' || !this.descriptorState.remainsUsable(descriptorSnapshot)) {
      protocolFailure('SPEND_INVALID', 'admission proof is invalid or crossed a descriptor fence')
    }
    const spendTag = asBytes(prepared.spendTag, 'spendTag', [32, 32])
    const preparedCommitment = asBytes(prepared.requestCommitment, 'prepared requestCommitment', [32, 32])
    const parameterHash = asBytes(prepared.parameterHash, 'prepared parameterHash', [32, 32])
    const walCommitRecord = asBytes(prepared.walCommitRecord, 'walCommitRecord', [1, MAX_WAL_COMMIT_RECORD_BYTES])
    const preparedCost = prepared.costClass
    if (!nonzero(spendTag) || !sameBytes(preparedCommitment, requestCommitment) ||
        prepared.profileId !== admission.profileId || prepared.schemeId !== admission.schemeId ||
        !sameBytes(parameterHash, admission.parameterHash) || !preparedCost ||
        preparedCost.resourceClass !== cost.resourceClass || preparedCost.leaseClass !== cost.leaseClass ||
        u64(preparedCost.costUnits, 'prepared costUnits') !== row.costUnits ||
        epoch(this.descriptorState) < parameters.validFromEpoch ||
        epoch(this.descriptorState) >= parameters.expiresEpoch) {
      protocolFailure('SPEND_INVALID', 'admission proof did not echo its exact current binding and cost')
    }
    return Object.freeze({
      spendTag: b4a.from(spendTag),
      requestCommitment: b4a.from(preparedCommitment),
      costClass: Object.freeze({
        resourceClass: cost.resourceClass,
        leaseClass: cost.leaseClass,
        costUnits: row.costUnits
      }),
      walCommitRecord: b4a.from(walCommitRecord),
      profileId: admission.profileId,
      schemeId: admission.schemeId,
      parameterHash: b4a.from(admission.parameterHash)
    })
  }
}