OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-protocol / schemas.js

1798 lines · javascript ref:
import b4a from 'b4a'
import {
  arrayOf,
  boundedBytes,
  canonicalAsciiBytes,
  canonicalHttpsUrlBytes,
  canonicalUtf8Bytes,
  constant,
  constantBytes,
  encodeCanonical,
  exactBytesByClass,
  fixedBytes,
  optional,
  ranged,
  struct,
  u8,
  u16be,
  u32be,
  u64be,
  validateCanonicalUrl
} from './codec.js'
import { protocolError } from './errors.js'
import { blake2b256, cellStorageSlot, inboxPhysicalTopic } from './hashes.js'
import { relayResultBindingV1 } from './result-binding.js'
import {
  admissionCostRule as lookupAdmissionCostRule,
  ADMISSION_CONFORMANCE_CLASS,
  CELL_RECEIPT_RESULT,
  CELL_SIZE_CLASS,
  CORE_ACK_RESULT,
  CORE_SESSION_CLASS,
  DISPATCH_LIMITS,
  DOMAIN_RECIPE,
  DOMAIN_PURPOSE,
  ERROR_PROFILE_ID,
  ERROR_RETRY_AFTER_MODE,
  ENDPOINT_LIMITS,
  FAMILY,
  FAMILY_ROUTES,
  FRAME_KIND,
  FORWARD_CLOSE_KIND,
  FORWARD_CIRCUIT_CLASS,
  HEALTH_CLOCK_STATE,
  HEALTH_INTEGRITY_STATE,
  HEALTH_REBALANCE_STATE,
  INBOX_APPEND_AUTH_MODE,
  INBOX_APPEND_RESULT,
  INBOX_FRAME_CLASS,
  INBOX_MANAGE_OPERATION,
  INBOX_RECEIPT_RESULT,
  PUBLIC_PROFILE_LIMITS,
  STORE_LIFECYCLE_STATE,
  STREAM_TRANSITION,
  STREAM_WIRE_CLASS,
  TRANSPORT_ID,
  TRANSPORT_SUPPORT,
  domainRegistryEntry as lookupDomainRegistryEntry,
  errorProfileEntry as lookupErrorProfileEntry,
  operationProfile as lookupOperationProfile
} from './wire-runtime-authority.js'
import { SCHEMA_CATALOG_NAME_HASHES_BY_CATEGORY } from './schema-catalog-runtime-authority.js'

const KiB = 1024
const MiB = 1024 * KiB
const MAX_U64 = (1n << 64n) - 1n

function fail (message) {
  protocolError('BAD_ENCODING', message)
}

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

function isAllZero (value) {
  for (let i = 0; i < value.byteLength; i++) {
    if (value[i] !== 0) return false
  }
  return true
}

function validateEpochWindow (value) {
  if (value.issuedEpoch >= value.expiresEpoch || value.expiresEpoch - value.issuedEpoch > 4) {
    fail('route epoch window must satisfy issuedEpoch < expiresEpoch <= issuedEpoch + 4')
  }
}

function validateForwardTuple (value) {
  const tuple = FORWARD_CIRCUIT_CLASS[value.circuitClass]
  if (!tuple) fail('circuitClass is outside 1..3')
  if (value.grantedInitialWindow !== tuple.grantedInitialWindow ||
      bigint(value.maxCircuitBytes, 'maxCircuitBytes') !== BigInt(tuple.maxCircuitBytes) ||
      value.idleMillis !== tuple.idleMillis || value.lifetimeMillis !== tuple.lifetimeMillis) {
    fail('forward circuit limits do not match the frozen class tuple')
  }
  if (value.maxDataBytes !== STREAM_WIRE_CLASS[value.grantedWireClass == null
    ? value.requestedWireClass
    : value.grantedWireClass]) {
    fail('maxDataBytes does not match the selected wire class')
  }
}

function validateCoreTuple (value) {
  const tuple = CORE_SESSION_CLASS[value.sessionClass]
  if (!tuple) fail('sessionClass is outside 1..3')
  if (bigint(value.maxSessionBytes, 'maxSessionBytes') !== BigInt(tuple.maxSessionBytes) ||
      value.idleMillis !== tuple.idleMillis || value.lifetimeMillis !== tuple.lifetimeMillis) {
    fail('core session limits do not match the frozen class tuple')
  }
}

const bytes16 = fixedBytes(16)
const bytes32 = fixedBytes(32)
const bytes64 = fixedBytes(64)
const bytes96 = fixedBytes(96)
const version1 = constant(u8, 1, 'version')
const profileId = ranged(u16be, 1, 0xffff, 'profileId')
const schemeId = ranged(u16be, 1, 0xffff, 'schemeId')
const endpointId = ranged(u8, 1, 0xff, 'endpointId')
const transportSupportBit = ranged(u16be, 1, 0x3f, 'transportSupportBit')
const wireClass = ranged(u8, 1, 3, 'wireClass')
const circuitClass = ranged(u8, 1, 3, 'circuitClass')
const sessionClass = ranged(u8, 1, 3, 'sessionClass')
const protocolId = ranged(u16be, 1, 5, 'protocolId')
const transportId = ranged(u8, 1, 9, 'transportId')
const cellSizeClass = ranged(u8, 1, 5, 'sizeClass')
const inboxFrameClass = ranged(u8, 1, 3, 'frameClass')
const leaseClass = ranged(u8, 1, 4, 'leaseClass')
const optionalLeaseClass = ranged(u8, 0, 4, 'leaseClass')
const cellBlob = exactBytesByClass('sizeClass', CELL_SIZE_CLASS, 'cellBlob')
const inboxFrame = exactBytesByClass('frameClass', INBOX_FRAME_CLASS, 'inbox frame')
const profileName = canonicalAsciiBytes(1, 64, 'profileName')
const endpointUrl = canonicalUtf8Bytes(1, 512, 'endpoint URL')
const buildManifestUrl = canonicalHttpsUrlBytes('buildManifestUrl')
const buildArtifactUrl = canonicalHttpsUrlBytes('buildArtifactUrl')
const releaseEvidenceBundleUrl = canonicalHttpsUrlBytes('releaseEvidenceBundleUrl')
const runtimeBoundaryEvidenceUrl = canonicalHttpsUrlBytes('runtimeBoundaryEvidenceUrl')
const externalJournalTopologyUrl = canonicalHttpsUrlBytes('externalJournalTopologyUrl')
const restoreEvidenceFeedUrl = canonicalHttpsUrlBytes('restoreEvidenceFeedUrl')
const cellProtocol = constantBytes(b4a.from('hiverelay-blind-cell-v1', 'ascii'), 'cell receipt protocol')

function bytesKey (value) {
  return b4a.toString(value, 'hex')
}

function assertDistinctBytes (values, name) {
  const seen = new Set()
  for (const value of values) {
    const key = bytesKey(value)
    if (seen.has(key)) fail(`${name} contains a duplicate`)
    seen.add(key)
  }
}

function compareBytes (a, b) {
  const length = Math.min(a.byteLength, b.byteLength)
  for (let index = 0; index < length; index++) {
    if (a[index] !== b[index]) return a[index] < b[index] ? -1 : 1
  }
  return a.byteLength === b.byteLength ? 0 : a.byteLength < b.byteLength ? -1 : 1
}

function compareScalar (a, b) {
  return a === b ? 0 : a < b ? -1 : 1
}

function assertStrictlySorted (values, compare, name) {
  for (let index = 1; index < values.length; index++) {
    if (compare(values[index - 1], values[index]) >= 0) fail(`${name} must be strictly sorted and duplicate-free`)
  }
}

function assertNonzeroBytes (value, name) {
  if (isAllZero(value)) fail(`${name} must be nonzero`)
}

function cappedEncoding (encoding, maximum, name) {
  const capped = {
    preencode (state, value) {
      const start = state.end
      encoding.preencode(state, value)
      if (state.end - start > maximum) fail(`${name} exceeds ${maximum} bytes`)
    },
    encode (state, value) {
      encoding.encode(state, value)
    },
    decode (state) {
      const start = state.start
      const value = encoding.decode(state)
      if (state.start - start > maximum) fail(`${name} exceeds ${maximum} bytes`)
      return value
    }
  }
  if (Array.isArray(encoding.schemaFields)) {
    Object.defineProperties(capped, {
      schemaName: {
        value: encoding.schemaName || name
      },
      schemaFields: {
        value: encoding.schemaFields
      }
    })
  }
  return capped
}

export const admissionCostRuleV1 = struct([
  ['costClassRuleId', ranged(u16be, 1, 11, 'costClassRuleId')],
  ['ruleKind', ranged(u8, 1, 11, 'ruleKind')]
], {
  name: 'AdmissionCostRuleV1',
  validate (value) {
    const expected = lookupAdmissionCostRule(value.costClassRuleId)
    if (!expected || expected.ruleKind !== value.ruleKind) fail('admission cost rule is not in the frozen registry')
  }
})

export const domainRegistryEntryV1 = struct([
  ['domainId', ranged(u16be, 1, 0xffff, 'domainId')],
  ['purpose', ranged(u8, DOMAIN_PURPOSE.REQUEST_COMMITMENT, DOMAIN_PURPOSE.AUXILIARY_SIGNATURE, 'domain purpose')],
  ['recipeId', ranged(u8, DOMAIN_RECIPE.OPERATION_DEFINED_COMMITMENT_PREIMAGE,
    DOMAIN_RECIPE.ED25519_DOMAIN_LEN64_PAYLOAD, 'domain recipe')],
  ['exactAsciiBytes', canonicalAsciiBytes(1, 96, 'exactAsciiBytes')]
], {
  name: 'DomainRegistryEntryV1',
  validate (value) {
    const expected = lookupDomainRegistryEntry(value.domainId)
    if (!expected || expected.purpose !== value.purpose || expected.recipeId !== value.recipeId ||
        !b4a.equals(b4a.from(expected.exactAsciiBytes, 'ascii'), value.exactAsciiBytes)) {
      fail('domain entry is not in the frozen registry')
    }
  }
})

export const errorProfileEntryV1 = struct([
  ['errorProfileId', constant(u8, ERROR_PROFILE_ID.CANONICAL_V1, 'errorProfileId')],
  ['code', ranged(u8, 1, 20, 'error code')],
  ['directCorrelatedStatus', constant(u16be, 200, 'directCorrelatedStatus')],
  ['protectedInnerStatus', constant(u16be, 200, 'protectedInnerStatus')],
  ['retryable', ranged(u8, 0, 1, 'retryable')],
  ['retryAfterMode', ranged(u8, ERROR_RETRY_AFTER_MODE.MUST_BE_ABSENT,
    ERROR_RETRY_AFTER_MODE.MUST_BE_PRESENT, 'retryAfterMode')]
], {
  name: 'ErrorProfileEntryV1',
  validate (value) {
    const expected = lookupErrorProfileEntry(value.errorProfileId, value.code)
    if (!expected || expected.directCorrelatedStatus !== value.directCorrelatedStatus ||
        expected.protectedInnerStatus !== value.protectedInnerStatus ||
        expected.retryable !== value.retryable || expected.retryAfterMode !== value.retryAfterMode) {
      fail('error profile entry is not in the frozen registry')
    }
  }
})

export const admissionV1 = struct([
  ['profileId', profileId],
  ['schemeId', schemeId],
  ['parameterHash', bytes32],
  ['token', boundedBytes(1, 4096, 'admission token')]
], { name: 'AdmissionV1' })

export const blindReceiptV1 = struct([
  ['version', version1],
  ['protocol', cellProtocol],
  ['relayBinding', relayResultBindingV1],
  ['slotCommitment', bytes32],
  ['cellBlobHash', bytes32],
  ['allocationCommitment', bytes32],
  ['requestCommitment', bytes32],
  ['sizeClass', cellSizeClass],
  ['allocationEpoch', u32be],
  ['leaseClass', optionalLeaseClass],
  ['leaseEpoch', u32be],
  ['stateRevision', u64be],
  ['receiptEpoch', u32be],
  ['requestNonce', bytes32],
  ['result', ranged(u8, CELL_RECEIPT_RESULT.STORED, CELL_RECEIPT_RESULT.DROPPED, 'cell receipt result')],
  ['signature', bytes64]
], {
  name: 'BlindReceiptV1',
  validate (value) {
    if (value.result === CELL_RECEIPT_RESULT.DROPPED) {
      if (value.leaseClass !== 0) fail('dropped cell receipt must use lease NONE')
    } else if (value.leaseClass === 0) fail('stored, served, or renewed cell receipt requires a lease')
  }
})

export const putCellV1 = struct([
  ['version', version1],
  ['storageSlot', bytes32],
  ['allocationEpoch', u32be],
  ['sizeClass', cellSizeClass],
  ['leaseClass', leaseClass],
  ['clientNonce', bytes32],
  ['createPublicKey', bytes32],
  ['renewPublicKey', bytes32],
  ['dropPublicKey', bytes32],
  ['declaredBlobHash', bytes32],
  ['createSignature', bytes64],
  ['admission', admissionV1],
  ['cellBlob', cellBlob]
], {
  name: 'PutCellV1',
  validate (value) {
    if (!b4a.equals(cellStorageSlot(value), value.storageSlot)) fail('storageSlot is not self-certifying')
    if (!b4a.equals(blake2b256(value.cellBlob), value.declaredBlobHash)) {
      fail('declaredBlobHash does not match cellBlob')
    }
  }
})

export const renewCellV1 = struct([
  ['version', version1],
  ['storageSlot', bytes32],
  ['expectedRevision', u64be],
  ['expectedLeaseEpoch', u32be],
  ['leaseClass', leaseClass],
  ['clientNonce', bytes32],
  ['admission', admissionV1],
  ['signature', bytes64]
], { name: 'RenewCellV1' })

export const dropCellV1 = struct([
  ['version', version1],
  ['storageSlot', bytes32],
  ['expectedRevision', u64be],
  ['expectedLeaseEpoch', u32be],
  ['clientNonce', bytes32],
  ['signature', bytes64]
], { name: 'DropCellV1' })

export const proveCellV1 = struct([
  ['version', version1],
  ['storageSlot', bytes32],
  ['clientNonce', bytes32],
  ['admission', optional(admissionV1, 'admission')]
], { name: 'ProveCellV1' })

export const getCellV1 = struct([
  ['version', version1],
  ['storageSlot', bytes32],
  ['clientNonce', bytes32],
  ['admission', optional(admissionV1, 'admission')]
], { name: 'GetCellV1' })

export const getCellResultV1 = struct([
  ['version', version1],
  ['sizeClass', cellSizeClass],
  ['cellBlob', cellBlob]
], { name: 'GetCellResultV1' })

export const proveCellResultV1 = struct([
  ['version', version1],
  ['receipt', blindReceiptV1],
  ['sizeClass', cellSizeClass],
  ['cellBlob', cellBlob]
], {
  name: 'ProveCellResultV1',
  validate (value) {
    if (value.receipt.result !== CELL_RECEIPT_RESULT.SERVED) fail('proof receipt result must be SERVED')
    if (value.receipt.sizeClass !== value.sizeClass) fail('proof sizeClass does not match receipt')
    if (!b4a.equals(blake2b256(value.cellBlob), value.receipt.cellBlobHash)) {
      fail('proof cellBlob does not match receipt hash')
    }
  }
})

const batchGetFoundV1 = struct([
  ['sizeClass', cellSizeClass],
  ['cellBlob', cellBlob]
], { name: 'BatchGetEntryV1.found' })

export const batchGetEntryV1 = {
  preencode (state, value) {
    if (!value || typeof value !== 'object') fail('BatchGetEntryV1 must be an object')
    u8.preencode(state, value.status)
    if (value.status === 0) return
    if (value.status !== 1) fail('BatchGetEntryV1 status must be 0 or 1')
    batchGetFoundV1.preencode(state, value)
  },
  encode (state, value) {
    if (!value || typeof value !== 'object') fail('BatchGetEntryV1 must be an object')
    u8.encode(state, value.status)
    if (value.status === 0) return
    if (value.status !== 1) fail('BatchGetEntryV1 status must be 0 or 1')
    batchGetFoundV1.encode(state, value)
  },
  decode (state) {
    const status = u8.decode(state)
    if (status === 0) return { status }
    if (status !== 1) fail('BatchGetEntryV1 status must be 0 or 1')
    return { status, ...batchGetFoundV1.decode(state) }
  }
}

const batchGetSlotsV1 = arrayOf(bytes32, 1, 64, 'batch slots')
const batchGetEntriesV1 = arrayOf(batchGetEntryV1, 1, 64, 'batch entries')

export function batchGetEntriesCommitment (entries) {
  return blake2b256(encodeCanonical(batchGetEntriesV1, entries))
}

export const batchGetV1 = struct([
  ['version', version1],
  ['clientNonce', bytes32],
  ['slots', batchGetSlotsV1],
  ['admission', optional(admissionV1, 'admission')]
], {
  name: 'BatchGetV1',
  validate (value) {
    assertDistinctBytes(value.slots, 'batch slots')
  }
})

export const batchGetResultV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['requestNonce', bytes32],
  ['requestCommitment', bytes32],
  ['entries', batchGetEntriesV1],
  ['entriesCommitment', bytes32],
  ['signature', bytes64]
], {
  name: 'BatchGetResultV1',
  validate (value) {
    const entriesBytes = encodeCanonical(batchGetEntriesV1, value.entries)
    const expected = blake2b256(entriesBytes)
    if (!b4a.equals(expected, value.entriesCommitment)) fail('batch entriesCommitment does not match entries')
    if (193 + entriesBytes.byteLength > 4 * MiB) fail('batch result exceeds its operation cap')
  }
})

export const inboxCreateV1 = struct([
  ['version', version1],
  ['allocationEpoch', u32be],
  ['physicalTopic', bytes32],
  ['frameClassBits', u8],
  ['appendAuthMode', ranged(u8, 0, 1, 'appendAuthMode')],
  ['createPublicKey', bytes32],
  ['appendPublicKey', optional(bytes32, 'appendPublicKey')],
  ['renewPublicKey', bytes32],
  ['closePublicKey', bytes32],
  ['retentionClass', leaseClass],
  ['leaseClass', leaseClass],
  ['clientNonce', bytes32],
  ['createSignature', bytes64],
  ['admission', admissionV1]
], {
  name: 'InboxCreateV1',
  validate (value) {
    if (!b4a.equals(inboxPhysicalTopic(value), value.physicalTopic)) fail('physicalTopic is not self-certifying')
    if (value.frameClassBits === 0 || (value.frameClassBits & ~0x07) !== 0) {
      fail('frameClassBits must contain only advertised inbox classes')
    }
    const signatureRequired = value.appendAuthMode === INBOX_APPEND_AUTH_MODE.SIGNATURE_REQUIRED
    if (signatureRequired !== (value.appendPublicKey != null)) {
      fail('appendPublicKey presence does not match appendAuthMode')
    }
  }
})

export const inboxManageV1 = struct([
  ['version', version1],
  ['operation', ranged(u8, 1, 2, 'inbox management operation')],
  ['physicalTopic', bytes32],
  ['expectedRevision', u64be],
  ['expectedLeaseEpoch', u32be],
  ['leaseClass', optionalLeaseClass],
  ['clientNonce', bytes32],
  ['signature', bytes64],
  ['admission', optional(admissionV1, 'admission')]
], {
  name: 'InboxManageV1',
  validate (value) {
    if (value.operation === INBOX_MANAGE_OPERATION.RENEW) {
      if (value.leaseClass === 0 || value.admission == null) fail('inbox renew requires lease and admission')
    } else if (value.leaseClass !== 0 || value.admission != null) {
      fail('inbox close requires lease NONE and no admission')
    }
  }
})

export const inboxAppendV1 = struct([
  ['version', version1],
  ['physicalTopic', bytes32],
  ['frameClass', inboxFrameClass],
  ['frameHash', bytes32],
  ['clientNonce', bytes32],
  ['appendSignature', optional(bytes64, 'appendSignature')],
  ['admission', admissionV1],
  ['frame', inboxFrame]
], {
  name: 'InboxAppendV1',
  validate (value) {
    if (!b4a.equals(blake2b256(value.frame), value.frameHash)) fail('frameHash does not match frame')
  }
})

export const inboxReadV1 = struct([
  ['version', version1],
  ['physicalTopic', bytes32],
  ['cursor', boundedBytes(0, 128, 'inbox cursor')],
  ['limit', ranged(u16be, 1, 64, 'inbox read limit')],
  ['clientNonce', bytes32],
  ['admission', optional(admissionV1, 'admission')]
], { name: 'InboxReadV1' })

export const inboxWatchV1 = struct([
  ['version', version1],
  ['physicalTopic', bytes32],
  ['afterRevision', u64be],
  ['limit', ranged(u16be, 1, 64, 'inbox watch limit')],
  ['maxWaitMillis', ranged(u16be, 1, 30000, 'maxWaitMillis')],
  ['clientNonce', bytes32],
  ['admission', admissionV1]
], { name: 'InboxWatchV1' })

export const inboxReceiptV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['topicCommitment', bytes32],
  ['stateRevision', u64be],
  ['leaseClass', optionalLeaseClass],
  ['leaseEpoch', u32be],
  ['requestNonce', bytes32],
  ['requestCommitment', bytes32],
  ['result', ranged(u8, 1, 3, 'inbox receipt result')],
  ['signature', bytes64]
], {
  name: 'InboxReceiptV1',
  validate (value) {
    if (value.result === INBOX_RECEIPT_RESULT.CLOSED) {
      if (value.leaseClass !== 0) fail('closed inbox receipt must use lease NONE')
    } else if (value.leaseClass === 0) fail('created or renewed inbox receipt requires a lease')
  }
})

export const inboxAppendAckV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['topicCommitment', bytes32],
  ['frameHash', bytes32],
  ['appendRevision', u64be],
  ['storedAtEpoch', u32be],
  ['expiresAtEpoch', u32be],
  ['requestNonce', bytes32],
  ['requestCommitment', bytes32],
  ['result', constant(u8, INBOX_APPEND_RESULT.STORED, 'result')],
  ['signature', bytes64]
], {
  name: 'InboxAppendAckV1',
  validate (value) {
    if (value.expiresAtEpoch <= value.storedAtEpoch) fail('expiresAtEpoch must be after storedAtEpoch')
  }
})

export const inboxReadEntryV1 = struct([
  ['appendRevision', u64be],
  ['frameHash', bytes32],
  ['frameClass', inboxFrameClass],
  ['frame', inboxFrame]
], {
  name: 'InboxReadResultV1.entry',
  validate (value) {
    if (!b4a.equals(blake2b256(value.frame), value.frameHash)) fail('entry frameHash does not match frame')
  }
})

const inboxReadEntriesV1 = arrayOf(inboxReadEntryV1, 0, 64, 'inbox read entries')
const inboxNextCursorV1 = optional(boundedBytes(0, 128, 'nextCursor'), 'nextCursor')

export function inboxReadEntriesCommitment (entries) {
  return blake2b256(encodeCanonical(inboxReadEntriesV1, entries))
}

export const inboxReadResultV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['requestNonce', bytes32],
  ['requestCommitment', bytes32],
  ['snapshotRevision', u64be],
  ['entries', inboxReadEntriesV1],
  ['entriesCommitment', bytes32],
  ['nextCursor', inboxNextCursorV1],
  ['signature', bytes64]
], {
  name: 'InboxReadResultV1',
  validate (value) {
    let previous = -1n
    for (const entry of value.entries) {
      const revision = bigint(entry.appendRevision, 'appendRevision')
      if (revision <= previous) fail('inbox entries must have strictly increasing appendRevision')
      if (revision > bigint(value.snapshotRevision, 'snapshotRevision')) {
        fail('inbox entry revision exceeds snapshotRevision')
      }
      previous = revision
    }
    const entriesBytes = encodeCanonical(inboxReadEntriesV1, value.entries)
    const expected = blake2b256(entriesBytes)
    if (!b4a.equals(expected, value.entriesCommitment)) fail('inbox entriesCommitment does not match entries')
    const resultBytes = 105 + entriesBytes.byteLength + 32 +
      encodeCanonical(inboxNextCursorV1, value.nextCursor).byteLength + 64
    if (resultBytes > 4 * MiB) fail('inbox read result exceeds its operation cap')
  }
})

export const blindErrorV1 = struct([
  ['version', version1],
  ['code', ranged(u8, 1, 20, 'error code')],
  ['retryable', ranged(u8, 0, 1, 'retryable')],
  ['retryAfterEpoch', optional(u32be, 'retryAfterEpoch')]
], {
  name: 'BlindErrorV1',
  validate (value) {
    const profile = lookupErrorProfileEntry(ERROR_PROFILE_ID.CANONICAL_V1, value.code)
    const retryAfterPresent = value.retryAfterEpoch != null
    if (!profile || profile.retryable !== value.retryable ||
        retryAfterPresent !== (profile.retryAfterMode === ERROR_RETRY_AFTER_MODE.MUST_BE_PRESENT)) {
      fail('blind error body does not match error profile 1')
    }
  }
})

export const blindOhttpTransportErrorV1 = struct([
  ['version', version1],
  ['code', ranged(u8, 1, 3, 'OHTTP transport error code')]
], { name: 'BlindOhttpTransportErrorV1' })

export const blindDescribeGetV1 = struct([
  ['version', version1],
  ['descriptorHash', optional(bytes32, 'descriptorHash')],
  ['clientNonce', bytes32]
], { name: 'BlindDescribeGetV1' })

export const blindAdmissionParametersRequestV1 = struct([
  ['version', version1],
  ['profileId', profileId],
  ['schemeId', schemeId],
  ['clientNonce', bytes32]
], { name: 'BlindAdmissionParametersRequestV1' })

export const operationProfileV1 = struct([
  ['familyId', ranged(u8, 1, 5, 'familyId')],
  ['operationId', ranged(u8, 1, 255, 'operationId')],
  ['requestSchemaId', ranged(u16be, 1, 0xffff, 'requestSchemaId')],
  ['resultSchemaId', u16be],
  ['allowedRequestKindBits', ranged(u8, 1, 0x0f, 'allowedRequestKindBits')],
  ['allowedResultKindBits', ranged(u8, 0, 0x0f, 'allowedResultKindBits')],
  ['streamTransition', ranged(u8, 0, 3, 'streamTransition')],
  ['maxRequestBodyBytes', ranged(u32be, 1, 4 * MiB, 'maxRequestBodyBytes')],
  ['maxResultBodyBytes', ranged(u32be, 1, 4 * MiB, 'maxResultBodyBytes')],
  ['admissionMode', ranged(u8, 0, 2, 'admissionMode')],
  ['costClassRuleId', u16be],
  ['requestCommitmentDomainId', u16be],
  ['resultSignatureDomainId', u16be],
  ['errorProfileId', constant(u8, 1, 'errorProfileId')],
  ['transportSupportBits', ranged(u16be, 1, 0x3f, 'transportSupportBits')]
], {
  name: 'OperationProfileV1',
  validate (value) {
    const requestDomain = value.requestCommitmentDomainId === 0
      ? null
      : lookupDomainRegistryEntry(value.requestCommitmentDomainId)
    const resultDomain = value.resultSignatureDomainId === 0
      ? null
      : lookupDomainRegistryEntry(value.resultSignatureDomainId)
    if (value.requestCommitmentDomainId !== 0 &&
        (!requestDomain || requestDomain.purpose !== DOMAIN_PURPOSE.REQUEST_COMMITMENT ||
         requestDomain.recipeId !== DOMAIN_RECIPE.OPERATION_DEFINED_COMMITMENT_PREIMAGE)) {
      fail('requestCommitmentDomainId is not a registered request domain')
    }
    if (value.resultSignatureDomainId !== 0 &&
        (!resultDomain || resultDomain.purpose !== DOMAIN_PURPOSE.RESULT_SIGNATURE ||
         resultDomain.recipeId !== DOMAIN_RECIPE.ED25519_DOMAIN_LEN64_PAYLOAD)) {
      fail('resultSignatureDomainId is not a registered result-signature domain')
    }
    if ((value.admissionMode === 0) !== (value.costClassRuleId === 0)) {
      fail('costClassRuleId zero state does not match admissionMode')
    }
    if (value.costClassRuleId !== 0 && !lookupAdmissionCostRule(value.costClassRuleId)) {
      fail('costClassRuleId is not in the frozen registry')
    }
    if (value.admissionMode === 2 && value.requestCommitmentDomainId === 0) {
      fail('required admission needs a request commitment domain')
    }
    if (value.resultSchemaId === 0 && value.streamTransition !== 3) {
      fail('only forward-active operations may omit resultSchemaId')
    }
    const requestKind = 1 << (FRAME_KIND.REQUEST - 1)
    const unaryResultKinds = (1 << (FRAME_KIND.RESPONSE - 1)) | (1 << (FRAME_KIND.ERROR - 1))
    const streamKind = 1 << (FRAME_KIND.STREAM - 1)
    const errorKind = 1 << (FRAME_KIND.ERROR - 1)
    if (value.streamTransition === STREAM_TRANSITION.FORWARD_ACTIVE) {
      if (value.familyId !== FAMILY.FORWARD || value.resultSchemaId !== 0 ||
          value.allowedRequestKindBits !== streamKind ||
          (value.allowedResultKindBits !== streamKind && value.allowedResultKindBits !== (streamKind | errorKind)) ||
          value.admissionMode !== 0 || value.costClassRuleId !== 0 ||
          value.requestCommitmentDomainId !== 0 || value.resultSignatureDomainId !== 0) {
        fail('forward-active operation metadata is inconsistent')
      }
    } else if (value.allowedRequestKindBits !== requestKind || value.allowedResultKindBits !== unaryResultKinds ||
        value.resultSchemaId === 0) {
      fail('unary/open operation metadata kind bits are inconsistent')
    }
    const nativeOnly = TRANSPORT_SUPPORT.DIRECT_NATIVE | TRANSPORT_SUPPORT.TOR_NATIVE
    const forwardNative = nativeOnly | TRANSPORT_SUPPORT.MASQUE_NATIVE
    if ((value.streamTransition === STREAM_TRANSITION.CORE_CHILD &&
         (value.familyId !== FAMILY.CORE || value.transportSupportBits !== nativeOnly)) ||
        ((value.streamTransition === STREAM_TRANSITION.FORWARD_OPEN ||
          value.streamTransition === STREAM_TRANSITION.FORWARD_ACTIVE) &&
         (value.familyId !== FAMILY.FORWARD || value.transportSupportBits !== forwardNative))) {
      fail('stream transition transport support is inconsistent')
    }
    const expected = lookupOperationProfile(value.familyId, value.operationId)
    if (!expected || Object.keys(expected).some(field => value[field] !== expected[field])) {
      fail('operation profile is not in the frozen registry')
    }
  }
})

export const protocolProfileArtifactV1 = struct([
  ['version', version1],
  ['protocolId', protocolId],
  ['major', u16be],
  ['minor', u16be],
  ['featureBits', u64be],
  ['wireSchemaSetHash', bytes32],
  ['dependencyManifestHash', bytes32],
  ['interoperabilityVectorSetHash', bytes32]
], { name: 'ProtocolProfileArtifactV1' })

export const transportProfileArtifactV1 = struct([
  ['version', version1],
  ['transportId', transportId],
  ['profileName', profileName],
  ['major', u16be],
  ['minor', u16be],
  ['exporterId', ranged(u8, 0, 1, 'exporterId')],
  ['controlChannelIdType', ranged(u8, 0, 1, 'controlChannelIdType')],
  ['handshakeProfileHash', bytes32],
  ['dependencyManifestHash', bytes32],
  ['interoperabilityVectorSetHash', bytes32]
], { name: 'TransportProfileArtifactV1' })

export const protocolProfileV1 = struct([
  ['protocolId', protocolId],
  ['major', u16be],
  ['minor', u16be],
  ['featureBits', u64be],
  ['profileHash', bytes32]
], { name: 'ProtocolProfileV1' })

export const transportEndpointV1 = struct([
  ['endpointId', endpointId],
  ['transportId', transportId],
  ['transportProfileHash', bytes32],
  ['roleBits', u16be],
  ['privacyProfileBits', u16be],
  ['canonicalUrl', endpointUrl],
  ['endpointKey', optional(bytes32, 'endpointKey')],
  ['envelopeClassBits', u16be],
  ['wireClassBits', u8],
  ['maxStreams', u16be],
  ['auxiliaryUrl', optional(endpointUrl, 'auxiliaryUrl')],
  ['auxiliaryHash', optional(bytes32, 'auxiliaryHash')]
], {
  name: 'TransportEndpointV1',
  validate (value) {
    if ((value.roleBits & ~ENDPOINT_LIMITS.ROLE_BITS_MASK) !== 0) fail('roleBits contains a reserved bit')
    if ((value.privacyProfileBits & ~ENDPOINT_LIMITS.PRIVACY_PROFILE_BITS_MASK) !== 0) {
      fail('privacyProfileBits contains a reserved bit')
    }
    if ((value.envelopeClassBits & ~ENDPOINT_LIMITS.ENVELOPE_CLASS_BITS_MASK) !== 0) {
      fail('envelopeClassBits contains a reserved bit')
    }
    if ((value.wireClassBits & ~ENDPOINT_LIMITS.WIRE_CLASS_BITS_MASK) !== 0) {
      fail('wireClassBits contains a reserved bit')
    }
    if ((value.wireClassBits === 0) !== (value.maxStreams === 0)) {
      fail('wireClassBits and maxStreams must both describe unary or streaming transport')
    }
    const onion = value.transportId === TRANSPORT_ID.TOR_V3_ONION
    validateCanonicalUrl(value.canonicalUrl, { name: 'canonicalUrl', requireOnion: onion })
    const listenerUrl = new URL(b4a.toString(value.canonicalUrl, 'utf8'))
    if (listenerUrl.pathname !== FAMILY_ROUTES[FAMILY.DESCRIBE]) {
      fail('canonicalUrl must use the generic listener authority anchor /api/blind/v1/describe')
    }
    if ((value.auxiliaryUrl == null) !== (value.auxiliaryHash == null)) {
      fail('auxiliaryUrl and auxiliaryHash must be present together')
    }
    if (onion && value.auxiliaryUrl != null) fail('Tor onion endpoints must not advertise an auxiliary URL')
    if (value.auxiliaryUrl != null) validateCanonicalUrl(value.auxiliaryUrl, { name: 'auxiliaryUrl' })
  }
})

export const durabilityProfileV1 = struct([
  ['profileId', ranged(u8, 1, 2, 'profileId')],
  ['storeFormatMajor', u16be],
  ['storeFormatMinor', u16be],
  ['storeFormatHash', bytes32],
  ['externalJournalId', bytes32],
  ['externalWitnessPublicKey', bytes32],
  ['externalJournalReplicationClass', ranged(u8, 0, 1, 'externalJournalReplicationClass')],
  ['externalJournalFailureGroupId', bytes32],
  ['externalCheckpointAgeBand', ranged(u8, 0, 7, 'externalCheckpointAgeBand')],
  ['externalJournalTopologyUrl', optional(externalJournalTopologyUrl, 'externalJournalTopologyUrl')],
  ['externalJournalTopologyHash', bytes32],
  ['restoreEvidenceFeedUrl', optional(restoreEvidenceFeedUrl, 'restoreEvidenceFeedUrl')],
  ['restoreEvidenceFeedId', bytes32],
  ['restoreEvidenceCheckpointSequence', u64be],
  ['restoreEvidenceCheckpointHash', bytes32],
  ['acknowledgedRpoBand', ranged(u8, 0, 3, 'acknowledgedRpoBand')],
  ['targetRtoBand', ranged(u8, 0, 3, 'targetRtoBand')],
  ['redundancyClass', ranged(u8, 0, 2, 'redundancyClass')],
  ['restoreDrillAgeBand', ranged(u8, 0, 7, 'restoreDrillAgeBand')]
], {
  name: 'DurabilityProfileV1',
  validate (value) {
    const externalFields = [
      value.externalJournalId,
      value.externalWitnessPublicKey,
      value.externalJournalFailureGroupId,
      value.externalJournalTopologyHash
    ]
    const restoreSequence = bigint(value.restoreEvidenceCheckpointSequence, 'restoreEvidenceCheckpointSequence')
    const restoreFeedZero = isAllZero(value.restoreEvidenceFeedId)
    const restoreHashZero = isAllZero(value.restoreEvidenceCheckpointHash)
    if (value.profileId === 1) {
      if (externalFields.some(value => !isAllZero(value)) || value.externalJournalReplicationClass !== 0 ||
          value.externalCheckpointAgeBand !== 0 || value.externalJournalTopologyUrl != null ||
          value.restoreEvidenceFeedUrl != null || !restoreFeedZero || restoreSequence !== 0n || !restoreHashZero ||
          value.acknowledgedRpoBand !== 0 || value.targetRtoBand !== 0 || value.redundancyClass !== 0 ||
          value.restoreDrillAgeBand !== 0) {
        fail('durability profile 1 requires the exact zero/absent external tuple')
      }
      return
    }
    if (externalFields.some(isAllZero) || value.externalJournalReplicationClass !== 1 ||
        value.externalJournalTopologyUrl == null) {
      fail('durability profile 2 requires its nonzero external journal topology tuple')
    }
    if ((value.restoreEvidenceFeedUrl == null) !== restoreFeedZero ||
        (restoreSequence === 0n) !== restoreHashZero ||
        (restoreFeedZero && (restoreSequence !== 0n || !restoreHashZero))) {
      fail('restore evidence feed URL, ID, sequence, and hash zero state is inconsistent')
    }
    const bodyRecoveryClaim = value.acknowledgedRpoBand !== 0 || value.targetRtoBand !== 0 ||
      value.redundancyClass !== 0 || value.restoreDrillAgeBand !== 0
    if (bodyRecoveryClaim && restoreFeedZero) fail('body recovery claims require a restore evidence feed')
  }
})

export const buildProfileV1 = struct([
  ['specHash', bytes32],
  ['abiHash', bytes32],
  ['vectorSetHash', bytes32],
  ['evidenceFormatHash', bytes32],
  ['evidenceVectorSetHash', bytes32],
  ['storeFormatHash', bytes32],
  ['storeVectorSetHash', bytes32],
  ['privateIpcFormatHash', bytes32],
  ['privateIpcVectorSetHash', bytes32],
  ['buildArtifactHash', bytes32],
  ['buildArtifactUrl', buildArtifactUrl],
  ['buildManifestUrl', buildManifestUrl],
  ['buildManifestHash', bytes32],
  ['releaseEvidenceBundleUrl', releaseEvidenceBundleUrl],
  ['releaseEvidenceBundleHash', bytes32],
  ['releaseSupportHorizonHash', bytes32],
  ['runtimeBoundaryEvidenceUrl', runtimeBoundaryEvidenceUrl],
  ['runtimeBoundaryEvidenceHash', bytes32]
], { name: 'BuildProfileV1' })

export const blindTransportRouteV1 = struct([
  ['version', version1],
  ['routeKind', ranged(u8, 1, 6, 'routeKind')],
  ['routeId', bytes16],
  ['previousRelayKey', bytes32],
  ['previousEndpointId', endpointId],
  ['nextRelayKey', bytes32],
  ['nextDescriptorSequence', u64be],
  ['nextDescriptorHash', bytes32],
  ['nextEndpointId', endpointId],
  ['envelopeClassBits', u16be],
  ['wireClassBits', u8],
  ['maxCanonicalDispatchBytes', u32be],
  ['maxEncapsulatedRequestBytes', u32be],
  ['maxOpenBytes', u32be],
  ['maxCircuitBytes', u64be],
  ['maxConcurrentStreams', u16be],
  ['maxRelayCount', ranged(u8, 2, 4, 'maxRelayCount')],
  ['hopAdmissionProfileId', profileId],
  ['issuedEpoch', u32be],
  ['expiresEpoch', u32be],
  ['routeNonce', bytes32],
  ['previousSignature', bytes64]
], {
  name: 'BlindTransportRouteV1',
  validate (value) {
    validateEpochWindow(value)
    if ((value.envelopeClassBits & ~0x7e) !== 0) fail('envelopeClassBits contains a reserved bit')
    if ((value.wireClassBits & ~0x0e) !== 0) fail('wireClassBits contains a reserved bit')
    if (value.maxCanonicalDispatchBytes > DISPATCH_LIMITS.MAX_WIRE_BYTES) {
      fail('maxCanonicalDispatchBytes exceeds the absolute dispatch cap')
    }
    if (value.maxOpenBytes > 128 * KiB) fail('maxOpenBytes exceeds 128 KiB')
    if (value.maxConcurrentStreams > 1024) fail('maxConcurrentStreams exceeds 1024')
    if (value.routeKind === 1) {
      if (value.envelopeClassBits === 0 || value.maxEncapsulatedRequestBytes === 0) {
        fail('OHTTP routes require envelope classes and an encapsulated request cap')
      }
      if (value.wireClassBits !== 0 || value.maxOpenBytes !== 0 ||
          bigint(value.maxCircuitBytes, 'maxCircuitBytes') !== 0n || value.maxConcurrentStreams !== 0) {
        fail('OHTTP routes must zero streaming-only fields')
      }
    } else {
      if (value.maxEncapsulatedRequestBytes !== 0) fail('non-OHTTP routes must zero maxEncapsulatedRequestBytes')
      if (value.routeKind >= 2 && value.routeKind <= 5 &&
          (value.wireClassBits === 0 || value.maxOpenBytes === 0 ||
           bigint(value.maxCircuitBytes, 'maxCircuitBytes') === 0n || value.maxConcurrentStreams === 0)) {
        fail('streaming routes require nonzero wire/open/circuit/stream caps')
      }
    }
  }
})

export const blindForwardRouteHopV1 = struct([
  ['hopIndex', ranged(u8, 0, 3, 'hopIndex')],
  ['relayPublicKey', bytes32],
  ['descriptorSequence', u64be],
  ['descriptorHash', bytes32],
  ['previousScopeHash', bytes32],
  ['scopeHash', bytes32],
  ['relaySignature', bytes64]
], {
  name: 'BlindForwardRouteHopV1',
  validate (value) {
    assertNonzeroBytes(value.relayPublicKey, 'relayPublicKey')
    assertNonzeroBytes(value.descriptorHash, 'descriptorHash')
    assertNonzeroBytes(value.scopeHash, 'scopeHash')
    if (value.hopIndex === 0 && !isAllZero(value.previousScopeHash)) {
      fail('route-scope hop zero requires an all-zero previousScopeHash')
    }
    if (value.hopIndex !== 0 && isAllZero(value.previousScopeHash)) {
      fail('route-scope continuation requires a nonzero previousScopeHash')
    }
  }
})

const blindForwardRouteHopsV1 = arrayOf(blindForwardRouteHopV1, 1, 4, 'forward route-scope hops')

export const blindForwardRouteScopeV1 = struct([
  ['version', version1],
  ['rootRouteId', bytes16],
  ['rootCircuitNonce', bytes32],
  ['rootRequestCommitment', bytes32],
  ['maxRelayCount', ranged(u8, 2, 4, 'maxRelayCount')],
  ['expiresEpoch', u32be],
  ['hops', blindForwardRouteHopsV1]
], {
  name: 'BlindForwardRouteScopeV1',
  validate (value) {
    assertNonzeroBytes(value.rootRouteId, 'rootRouteId')
    assertNonzeroBytes(value.rootCircuitNonce, 'rootCircuitNonce')
    assertNonzeroBytes(value.rootRequestCommitment, 'rootRequestCommitment')
    if (value.expiresEpoch === 0) fail('route-scope expiresEpoch must be nonzero')
    if (value.hops.length > value.maxRelayCount) fail('route-scope relay count exceeds maxRelayCount')
    const relayKeys = new Set()
    for (let index = 0; index < value.hops.length; index++) {
      const hop = value.hops[index]
      if (hop.hopIndex !== index) fail('route-scope hop indexes must be contiguous from zero')
      if (index > 0 && !b4a.equals(hop.previousScopeHash, value.hops[index - 1].scopeHash)) {
        fail('route-scope previousScopeHash does not bind the complete prefix')
      }
      const relayKey = b4a.toString(hop.relayPublicKey, 'hex')
      if (relayKeys.has(relayKey)) fail('route-scope repeats a relay public key')
      relayKeys.add(relayKey)
    }
  }
})

export const blindForwardHopOpenV1 = struct([
  ['version', version1],
  ['route', blindTransportRouteV1],
  ['routeScope', blindForwardRouteScopeV1],
  ['previousDescriptorSequence', u64be],
  ['previousDescriptorHash', bytes32],
  ['circuitNonce', bytes32],
  ['requestedWireClass', wireClass],
  ['circuitClass', circuitClass],
  ['grantedInitialWindow', u32be],
  ['maxDataBytes', u32be],
  ['maxCircuitBytes', u64be],
  ['idleMillis', u32be],
  ['lifetimeMillis', u32be],
  ['clientRequestCommitment', bytes32],
  ['handshakeFlight1', bytes32],
  ['forwarderSignature', bytes64]
], {
  name: 'BlindForwardHopOpenV1',
  validate (value) {
    validateForwardTuple(value)
    if (value.route.routeKind < 2 || value.route.routeKind > 5) {
      fail('BlindForwardHopOpenV1 requires a streaming route')
    }
    if ((value.route.wireClassBits & (1 << value.requestedWireClass)) === 0) {
      fail('requestedWireClass is not admitted by the route')
    }
    if (bigint(value.maxCircuitBytes, 'maxCircuitBytes') > bigint(value.route.maxCircuitBytes, 'route.maxCircuitBytes')) {
      fail('forward circuit exceeds the route byte cap')
    }
    const lastHop = value.routeScope.hops[value.routeScope.hops.length - 1]
    if (!b4a.equals(lastHop.relayPublicKey, value.route.previousRelayKey) ||
        bigint(lastHop.descriptorSequence, 'routeScope descriptorSequence') !==
          bigint(value.previousDescriptorSequence, 'previousDescriptorSequence') ||
        !b4a.equals(lastHop.descriptorHash, value.previousDescriptorHash)) {
      fail('BlindForwardHopOpenV1 route scope does not end at the forwarding relay descriptor')
    }
    if (value.routeScope.hops.length > value.route.maxRelayCount) {
      fail('BlindForwardHopOpenV1 route scope exceeds the signed route relay bound')
    }
  }
})

export const blindForwardHopAcceptV1 = struct([
  ['version', version1],
  ['previousRelayKey', bytes32],
  ['previousDescriptorSequence', u64be],
  ['previousDescriptorHash', bytes32],
  ['nextRelayKey', bytes32],
  ['nextDescriptorSequence', u64be],
  ['nextDescriptorHash', bytes32],
  ['nextRelayBinding', relayResultBindingV1],
  ['routeId', bytes16],
  ['circuitNonce', bytes32],
  ['nextStreamId', u64be],
  ['grantedWireClass', wireClass],
  ['circuitClass', circuitClass],
  ['grantedInitialWindow', u32be],
  ['maxDataBytes', u32be],
  ['maxCircuitBytes', u64be],
  ['idleMillis', u32be],
  ['lifetimeMillis', u32be],
  ['openedAtEpoch', u32be],
  ['hopOpenCommitment', bytes32],
  ['acceptedRouteScopeHash', bytes32],
  ['acceptedRelayCount', ranged(u8, 1, 4, 'acceptedRelayCount')],
  ['handshakeFlight2', bytes96],
  ['nextSignature', bytes64]
], {
  name: 'BlindForwardHopAcceptV1',
  validate (value) {
    validateForwardTuple(value)
    if (bigint(value.nextStreamId, 'nextStreamId') === 0n) fail('nextStreamId must be nonzero')
    if (!b4a.equals(value.nextRelayKey, value.nextRelayBinding.relayPublicKey) ||
        bigint(value.nextDescriptorSequence, 'nextDescriptorSequence') !==
          bigint(value.nextRelayBinding.descriptorSequence, 'binding descriptorSequence') ||
        !b4a.equals(value.nextDescriptorHash, value.nextRelayBinding.descriptorHash)) {
      fail('nextRelayBinding does not match the accepted next relay descriptor')
    }
  }
})

export const coreOpenReplicationV1 = struct([
  ['version', version1],
  ['wireProfileHash', bytes32],
  ['sessionClass', sessionClass],
  ['controlChannelId', u64be],
  ['parentChannelBinding', bytes32],
  ['clientNonce', bytes32],
  ['admission', admissionV1]
], {
  name: 'CoreOpenReplicationV1',
  validate (value) {
    if (bigint(value.controlChannelId, 'controlChannelId') === 0n) fail('controlChannelId must be nonzero')
    if (isAllZero(value.parentChannelBinding)) fail('parentChannelBinding must be nonzero')
  }
})

export const coreOpenReplicationResultV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['wireProfileHash', bytes32],
  ['sessionClass', sessionClass],
  ['controlChannelId', u64be],
  ['parentChannelBinding', bytes32],
  ['streamId', u64be],
  ['maxSessionBytes', u64be],
  ['idleMillis', u32be],
  ['lifetimeMillis', u32be],
  ['openedAtEpoch', u32be],
  ['requestNonce', bytes32],
  ['requestCommitment', bytes32],
  ['signature', bytes64]
], {
  name: 'CoreOpenReplicationResultV1',
  validate (value) {
    validateCoreTuple(value)
    if (bigint(value.controlChannelId, 'controlChannelId') === 0n) fail('controlChannelId must be nonzero')
    if (bigint(value.streamId, 'streamId') === 0n) fail('streamId must be nonzero')
    if (isAllZero(value.parentChannelBinding)) fail('parentChannelBinding must be nonzero')
  }
})

// Kept here for subsequent operation codecs so every fixed identity field uses
// the same zero-copy primitive and no schema silently chooses another encoding.
export const fixed16 = bytes16
export const fixed32 = bytes32

const conformanceClass = ranged(u8, ADMISSION_CONFORMANCE_CLASS.OPEN,
  ADMISSION_CONFORMANCE_CLASS.PRIVATE, 'conformanceClass')
const roleBits = u16be
const operationBits = u32be
const parameterUrl = canonicalHttpsUrlBytes('parameterUrl')
const issuanceUrl = canonicalHttpsUrlBytes('issuanceUrl')
const descriptorUrl = canonicalHttpsUrlBytes('descriptorUrl')

export const admissionProfileV1 = struct([
  ['profileId', profileId],
  ['schemeId', schemeId],
  ['conformanceClass', conformanceClass],
  ['roleBits', roleBits],
  ['parameterUrl', optional(parameterUrl, 'parameterUrl')],
  ['parameterHash', bytes32]
], {
  name: 'AdmissionProfileV1',
  validate (value) {
    if (value.roleBits === 0 || (value.roleBits & ~ENDPOINT_LIMITS.ROLE_BITS_MASK) !== 0) {
      fail('admission roleBits must contain known nonzero bits')
    }
  }
})

export const admissionResourceCostV1 = struct([
  ['familyId', ranged(u8, 1, 5, 'resource cost familyId')],
  ['operationId', ranged(u8, 1, 255, 'resource cost operationId')],
  ['resourceClass', u8],
  ['leaseClass', optionalLeaseClass],
  ['costUnits', u64be]
], {
  name: 'AdmissionParametersV1.resourceCost',
  validate (value) {
    if (bigint(value.costUnits, 'costUnits') === 0n) fail('costUnits must be nonzero')
  }
})

const admissionResourceCostsV1 = arrayOf(admissionResourceCostV1, 1,
  PUBLIC_PROFILE_LIMITS.MAX_ADMISSION_RESOURCE_COST_ROWS, 'admission resource costs')

function compareResourceCost (a, b) {
  for (const field of ['familyId', 'operationId', 'resourceClass', 'leaseClass']) {
    const compared = compareScalar(a[field], b[field])
    if (compared !== 0) return compared
  }
  return 0
}

const admissionParametersBaseV1 = struct([
  ['version', version1],
  ['relayPublicKey', bytes32],
  ['profileId', profileId],
  ['schemeId', schemeId],
  ['conformanceClass', conformanceClass],
  ['roleBits', roleBits],
  ['verifierKey', boundedBytes(0, 4096, 'verifierKey')],
  ['resourceCosts', admissionResourceCostsV1],
  ['tokenMaxBytes', ranged(u16be, 1, PUBLIC_PROFILE_LIMITS.MAX_ADMISSION_TOKEN_BYTES, 'tokenMaxBytes')],
  ['issuanceUrl', optional(issuanceUrl, 'issuanceUrl')],
  ['issuerRelayKey', optional(bytes32, 'issuerRelayKey')],
  ['validFromEpoch', u32be],
  ['expiresEpoch', u32be],
  ['nonce', bytes32],
  ['signature', bytes64]
], {
  name: 'AdmissionParametersV1',
  validate (value) {
    assertNonzeroBytes(value.relayPublicKey, 'relayPublicKey')
    if (value.roleBits === 0 || (value.roleBits & ~ENDPOINT_LIMITS.ROLE_BITS_MASK) !== 0) {
      fail('admission roleBits must contain known nonzero bits')
    }
    assertStrictlySorted(value.resourceCosts, compareResourceCost, 'admission resource costs')
    if ((value.issuanceUrl == null) !== (value.issuerRelayKey == null)) {
      fail('issuanceUrl and issuerRelayKey must be present together')
    }
    if (value.issuerRelayKey != null) assertNonzeroBytes(value.issuerRelayKey, 'issuerRelayKey')
    if (value.validFromEpoch >= value.expiresEpoch) fail('admission parameter validity window must be nonempty')
  }
})

export const admissionParametersV1 = cappedEncoding(admissionParametersBaseV1, 16 * KiB, 'AdmissionParametersV1')

export const relayIdentityTransitionV1 = struct([
  ['version', version1],
  ['oldRelayKey', bytes32],
  ['newRelayKey', bytes32],
  ['oldIdentitySequence', u64be],
  ['newIdentitySequence', u64be],
  ['validFromEpoch', u32be],
  ['reasonCode', ranged(u8, 1, 3, 'identity transition reasonCode')],
  ['transitionNonce', bytes32],
  ['oldSignature', bytes64],
  ['newSignature', bytes64]
], {
  name: 'RelayIdentityTransitionV1',
  validate (value) {
    assertNonzeroBytes(value.oldRelayKey, 'oldRelayKey')
    assertNonzeroBytes(value.newRelayKey, 'newRelayKey')
    if (b4a.equals(value.oldRelayKey, value.newRelayKey)) fail('identity transition relay keys must differ')
    if (bigint(value.oldIdentitySequence, 'oldIdentitySequence') === MAX_U64 ||
        bigint(value.newIdentitySequence, 'newIdentitySequence') !==
          bigint(value.oldIdentitySequence, 'oldIdentitySequence') + 1n) {
      fail('newIdentitySequence must equal oldIdentitySequence + 1')
    }
  }
})

const blindDhtPointerBaseV1 = struct([
  ['version', version1],
  ['relayPublicKey', bytes32],
  ['descriptorSequence', u64be],
  ['descriptorHash', bytes32],
  ['descriptorUrl', descriptorUrl],
  ['transportBits', u16be],
  ['issuedEpoch', u32be],
  ['expiresEpoch', u32be],
  ['nonce', bytes32],
  ['signature', bytes64]
], {
  name: 'BlindDhtPointerV1',
  validate (value) {
    assertNonzeroBytes(value.relayPublicKey, 'relayPublicKey')
    if (value.transportBits === 0 || (value.transportBits & ~PUBLIC_PROFILE_LIMITS.TRANSPORT_BITS_MASK) !== 0) {
      fail('transportBits must contain known nonzero bits')
    }
    validateEpochWindow(value)
  }
})

export const blindDhtPointerV1 = cappedEncoding(blindDhtPointerBaseV1,
  PUBLIC_PROFILE_LIMITS.MAX_DHT_POINTER_BYTES, 'BlindDhtPointerV1')

export const blindOhttpKeyConfigV1 = struct([
  ['version', version1],
  ['gatewayRelayKey', bytes32],
  ['gatewayDescriptorSequence', u64be],
  ['configId', u8],
  ['kemId', ranged(u16be, 1, 0xffff, 'kemId')],
  ['kdfId', ranged(u16be, 1, 0xffff, 'kdfId')],
  ['aeadId', ranged(u16be, 1, 0xffff, 'aeadId')],
  ['encodedPublicKey', boundedBytes(1, 256, 'encodedPublicKey')],
  ['notBeforeEpoch', u32be],
  ['notAfterEpoch', u32be],
  ['previousConfigHash', optional(bytes32, 'previousConfigHash')],
  ['signature', bytes64]
], {
  name: 'BlindOhttpKeyConfigV1',
  validate (value) {
    assertNonzeroBytes(value.gatewayRelayKey, 'gatewayRelayKey')
    if (value.notBeforeEpoch >= value.notAfterEpoch ||
        value.notAfterEpoch - value.notBeforeEpoch > PUBLIC_PROFILE_LIMITS.MAX_OHTTP_CONFIG_EPOCHS) {
      fail('OHTTP key validity must be within 1..120 epochs')
    }
  }
})

export const blindHealthChallengeV1 = struct([
  ['version', version1],
  ['descriptorSequence', u64be],
  ['descriptorHash', bytes32],
  ['endpointId', endpointId],
  ['transportSupportBit', transportSupportBit],
  ['requestedRoleBits', roleBits],
  ['requestedOperationBits', operationBits],
  ['clientNonce', bytes32]
], {
  name: 'BlindHealthChallengeV1',
  validate (value) {
    if ((value.transportSupportBit & (value.transportSupportBit - 1)) !== 0) {
      fail('transportSupportBit must be one frozen one-hot support bit')
    }
    if ((value.requestedRoleBits & ~ENDPOINT_LIMITS.ROLE_BITS_MASK) !== 0) {
      fail('requestedRoleBits contains a reserved bit')
    }
    if ((value.requestedOperationBits & ~PUBLIC_PROFILE_LIMITS.ENABLED_OPERATION_BITS_MASK) !== 0) {
      fail('requestedOperationBits contains a reserved bit')
    }
    if (value.requestedRoleBits === 0 || value.requestedOperationBits === 0) {
      fail('health challenge role and operation bitmaps must both be nonzero')
    }
  }
})

export const blindHealthResultV1 = struct([
  ['version', version1],
  ['relayPublicKey', bytes32],
  ['storeId', bytes32],
  ['descriptorSequence', u64be],
  ['descriptorHash', bytes32],
  ['endpointId', endpointId],
  ['transportSupportBit', transportSupportBit],
  ['durabilityContinuityHash', bytes32],
  ['durabilityProfileHash', bytes32],
  ['clientNonce', bytes32],
  ['readyRoleBits', roleBits],
  ['readyOperationBits', operationBits],
  ['clockState', ranged(u8, HEALTH_CLOCK_STATE.READY, HEALTH_CLOCK_STATE.VERIFYING, 'clockState')],
  ['effectiveEpochFloor', u32be],
  ['integrityState', ranged(u8, HEALTH_INTEGRITY_STATE.VERIFIED, HEALTH_INTEGRITY_STATE.FAILED, 'integrityState')],
  ['checkpointAgeBand', ranged(u8, 0, 7, 'checkpointAgeBand')],
  ['scrubAgeBand', ranged(u8, 0, 7, 'scrubAgeBand')],
  ['rebalanceState', ranged(u8, HEALTH_REBALANCE_STATE.STABLE, HEALTH_REBALANCE_STATE.FENCED, 'rebalanceState')],
  ['capacityBand', ranged(u8, 0, 7, 'capacityBand')],
  ['challengeEpoch', u32be],
  ['signature', bytes64]
], {
  name: 'BlindHealthResultV1',
  validate (value) {
    assertNonzeroBytes(value.relayPublicKey, 'relayPublicKey')
    assertNonzeroBytes(value.storeId, 'storeId')
    if ((value.transportSupportBit & (value.transportSupportBit - 1)) !== 0) {
      fail('transportSupportBit must be one frozen one-hot support bit')
    }
    if ((value.readyRoleBits & ~ENDPOINT_LIMITS.ROLE_BITS_MASK) !== 0) {
      fail('readyRoleBits contains a reserved bit')
    }
    if ((value.readyOperationBits & ~PUBLIC_PROFILE_LIMITS.ENABLED_OPERATION_BITS_MASK) !== 0) {
      fail('readyOperationBits contains a reserved bit')
    }
  }
})

export const blindExternalBackupFailureGroupV1 = struct([
  ['backupFailureGroupId', bytes32],
  ['operatorGroupId', bytes32]
], {
  name: 'BlindExternalJournalTopologyV1.backupFailureGroup',
  validate (value) {
    assertNonzeroBytes(value.backupFailureGroupId, 'backupFailureGroupId')
    assertNonzeroBytes(value.operatorGroupId, 'backup operatorGroupId')
  }
})

export const blindExternalJournalNodeV1 = struct([
  ['nodePublicKey', bytes32],
  ['operatorGroupId', bytes32],
  ['failureDomainId', bytes32],
  ['roleConflictBits', u16be]
], {
  name: 'BlindExternalJournalTopologyV1.node',
  validate (value) {
    assertNonzeroBytes(value.nodePublicKey, 'journal nodePublicKey')
    assertNonzeroBytes(value.operatorGroupId, 'journal operatorGroupId')
    assertNonzeroBytes(value.failureDomainId, 'journal failureDomainId')
  }
})

const externalBackupFailureGroupsV1 = arrayOf(blindExternalBackupFailureGroupV1, 0, 16,
  'external backup failure groups')
const externalJournalNodesV1 = arrayOf(blindExternalJournalNodeV1, 3, 3, 'external journal nodes')

function compareBackupFailureGroup (a, b) {
  return compareBytes(a.backupFailureGroupId, b.backupFailureGroupId) ||
    compareBytes(a.operatorGroupId, b.operatorGroupId)
}

function compareJournalNode (a, b) {
  return compareBytes(a.nodePublicKey, b.nodePublicKey) ||
    compareBytes(a.operatorGroupId, b.operatorGroupId) ||
    compareBytes(a.failureDomainId, b.failureDomainId) ||
    compareScalar(a.roleConflictBits, b.roleConflictBits)
}

export const blindExternalJournalTopologyV1 = struct([
  ['version', version1],
  ['relayPublicKey', bytes32],
  ['storeId', bytes32],
  ['externalJournalId', bytes32],
  ['durabilityContinuityHash', bytes32],
  ['topologySequence', u64be],
  ['previousTopologyHash', optional(bytes32, 'previousTopologyHash')],
  ['replicationClass', constant(u8, 1, 'replicationClass')],
  ['commitQuorum', constant(u8, 2, 'commitQuorum')],
  ['sharedFailureGroupId', bytes32],
  ['liveStoreFailureGroupId', bytes32],
  ['backupFailureGroups', externalBackupFailureGroupsV1],
  ['nodes', externalJournalNodesV1],
  ['issuedEpoch', u32be],
  ['expiresEpoch', u32be],
  ['witnessPublicKey', bytes32],
  ['signature', bytes64]
], {
  name: 'BlindExternalJournalTopologyV1',
  validate (value) {
    for (const field of [
      'relayPublicKey',
      'storeId',
      'externalJournalId',
      'durabilityContinuityHash',
      'sharedFailureGroupId',
      'liveStoreFailureGroupId',
      'witnessPublicKey'
    ]) assertNonzeroBytes(value[field], field)
    const sequence = bigint(value.topologySequence, 'topologySequence')
    if ((sequence === 0n) !== (value.previousTopologyHash == null)) {
      fail('previousTopologyHash must be absent exactly for topology sequence zero')
    }
    assertStrictlySorted(value.backupFailureGroups, compareBackupFailureGroup, 'backup failure groups')
    assertStrictlySorted(value.nodes, compareJournalNode, 'external journal nodes')
    assertDistinctBytes(value.nodes.map(node => node.operatorGroupId), 'journal operator groups')
    assertDistinctBytes(value.nodes.map(node => node.failureDomainId), 'journal failure domains')
    validateEpochWindow(value)
  }
})

export const blindCoreAckV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['corePublicKey', bytes32],
  ['fork', u64be],
  ['length', u64be],
  ['signedHeadHash', bytes32],
  ['observedAtEpoch', u32be],
  ['leaseEpoch', u32be],
  ['result', ranged(u8, CORE_ACK_RESULT.MIRROR_ACCEPTED, CORE_ACK_RESULT.RECENTLY_SERVED, 'core ack result')],
  ['requestNonce', bytes32],
  ['requestCommitment', bytes32],
  ['signature', bytes64]
], {
  name: 'BlindCoreAckV1',
  validate (value) {
    assertNonzeroBytes(value.corePublicKey, 'corePublicKey')
    assertNonzeroBytes(value.signedHeadHash, 'signedHeadHash')
  }
})

export const coreMirrorRequestV1 = struct([
  ['version', version1],
  ['corePublicKey', bytes32],
  ['fork', u64be],
  ['length', u64be],
  ['signedHeadHash', bytes32],
  ['leaseClass', leaseClass],
  ['clientNonce', bytes32],
  ['admission', admissionV1]
], {
  name: 'CoreMirrorRequestV1',
  validate (value) {
    assertNonzeroBytes(value.corePublicKey, 'corePublicKey')
    assertNonzeroBytes(value.signedHeadHash, 'signedHeadHash')
    if (bigint(value.length, 'length') === 0n) fail('core mirror length must be nonzero')
  }
})

const coreBlockIndicesV1 = arrayOf(u64be, 1, 16, 'core block indices')

export const coreServeChallengeV1 = struct([
  ['version', version1],
  ['corePublicKey', bytes32],
  ['fork', u64be],
  ['length', u64be],
  ['signedHeadHash', bytes32],
  ['blockIndices', coreBlockIndicesV1],
  ['clientNonce', bytes32],
  ['admission', optional(admissionV1, 'admission')]
], {
  name: 'CoreServeChallengeV1',
  validate (value) {
    assertNonzeroBytes(value.corePublicKey, 'corePublicKey')
    assertNonzeroBytes(value.signedHeadHash, 'signedHeadHash')
    const length = bigint(value.length, 'length')
    if (length === 0n) fail('core serve length must be nonzero')
    assertStrictlySorted(value.blockIndices, (a, b) => compareScalar(bigint(a, 'block index'), bigint(b, 'block index')),
      'core block indices')
    if (value.blockIndices.some(index => bigint(index, 'block index') >= length)) {
      fail('core block index must be below length')
    }
  }
})

const coreServeResultBaseV1 = struct([
  ['version', version1],
  ['acknowledgement', blindCoreAckV1],
  ['proofsAndBlocks', boundedBytes(1, 4 * MiB - 256, 'proofsAndBlocks')]
], {
  name: 'CoreServeResultV1',
  validate (value) {
    if (value.acknowledgement.result !== CORE_ACK_RESULT.RECENTLY_SERVED) {
      fail('core serve acknowledgement result must be RECENTLY_SERVED')
    }
  }
})

export const coreServeResultV1 = cappedEncoding(coreServeResultBaseV1, 4 * MiB, 'CoreServeResultV1')

export const blindForwardOpenV1 = struct([
  ['version', version1],
  ['routeId', bytes16],
  ['nextDescriptorSequence', u64be],
  ['nextDescriptorHash', bytes32],
  ['requestedWireClass', wireClass],
  ['circuitClass', circuitClass],
  ['circuitNonce', bytes32],
  ['parentRouteScopeHash', bytes32],
  ['hopAdmission', admissionV1],
  ['innerHandshake', boundedBytes(32, 32, 'innerHandshake')]
], {
  name: 'BlindForwardOpenV1',
  validate (value) {
    assertNonzeroBytes(value.routeId, 'routeId')
    assertNonzeroBytes(value.nextDescriptorHash, 'nextDescriptorHash')
    assertNonzeroBytes(value.circuitNonce, 'circuitNonce')
  }
})

export const blindForwardOpenResultV1 = struct([
  ['version', version1],
  ['relayBinding', relayResultBindingV1],
  ['routeId', bytes16],
  ['nextDescriptorSequence', u64be],
  ['nextDescriptorHash', bytes32],
  ['circuitNonce', bytes32],
  ['grantedWireClass', wireClass],
  ['circuitClass', circuitClass],
  ['streamId', u64be],
  ['grantedInitialWindow', u32be],
  ['maxDataBytes', u32be],
  ['maxCircuitBytes', u64be],
  ['idleMillis', u32be],
  ['lifetimeMillis', u32be],
  ['openedAtEpoch', u32be],
  ['requestCommitment', bytes32],
  ['acceptedRouteScopeHash', bytes32],
  ['acceptedRelayCount', ranged(u8, 1, 4, 'acceptedRelayCount')],
  ['nextHopAccept', blindForwardHopAcceptV1],
  ['signature', bytes64]
], {
  name: 'BlindForwardOpenResultV1',
  validate (value) {
    validateForwardTuple(value)
    if (bigint(value.streamId, 'streamId') === 0n) fail('streamId must be nonzero')
    for (const field of ['routeId', 'nextDescriptorHash', 'circuitNonce', 'requestCommitment']) {
      assertNonzeroBytes(value[field], field)
    }
    const next = value.nextHopAccept
    if (!b4a.equals(value.relayBinding.relayPublicKey, next.previousRelayKey) ||
        !b4a.equals(value.routeId, next.routeId) ||
        !b4a.equals(value.nextDescriptorHash, next.nextDescriptorHash) ||
        !b4a.equals(value.circuitNonce, next.circuitNonce) ||
        bigint(value.nextDescriptorSequence, 'nextDescriptorSequence') !==
          bigint(next.nextDescriptorSequence, 'nextDescriptorSequence') ||
        value.grantedWireClass !== next.grantedWireClass || value.circuitClass !== next.circuitClass ||
        value.grantedInitialWindow !== next.grantedInitialWindow || value.maxDataBytes !== next.maxDataBytes ||
        bigint(value.maxCircuitBytes, 'maxCircuitBytes') !== bigint(next.maxCircuitBytes, 'next maxCircuitBytes') ||
        value.idleMillis !== next.idleMillis || value.lifetimeMillis !== next.lifetimeMillis ||
        value.openedAtEpoch !== next.openedAtEpoch ||
        !b4a.equals(value.acceptedRouteScopeHash, next.acceptedRouteScopeHash) ||
        value.acceptedRelayCount !== next.acceptedRelayCount) {
      fail('forward open result does not match nextHopAccept')
    }
  }
})

export const blindForwardDataV1 = struct([
  ['version', version1],
  ['circuitNonce', bytes32],
  ['offset', u64be],
  ['bytes', boundedBytes(1, DISPATCH_LIMITS.MAX_FORWARD_DATA_BYTES, 'forward data bytes')]
], {
  name: 'BlindForwardDataV1',
  validate (value) {
    assertNonzeroBytes(value.circuitNonce, 'circuitNonce')
  }
})

export const blindForwardWindowV1 = struct([
  ['version', version1],
  ['circuitNonce', bytes32],
  ['consumedThrough', u64be],
  ['creditIncrement', ranged(u32be, 1, DISPATCH_LIMITS.MAX_FORWARD_WINDOW_BYTES, 'creditIncrement')]
], {
  name: 'BlindForwardWindowV1',
  validate (value) {
    assertNonzeroBytes(value.circuitNonce, 'circuitNonce')
  }
})

export const blindForwardCloseV1 = struct([
  ['version', version1],
  ['circuitNonce', bytes32],
  ['closeKind', ranged(u8, FORWARD_CLOSE_KIND.FIN, FORWARD_CLOSE_KIND.ABORT, 'closeKind')],
  ['finalSendOffset', u64be],
  ['reasonCode', u8]
], {
  name: 'BlindForwardCloseV1',
  validate (value) {
    assertNonzeroBytes(value.circuitNonce, 'circuitNonce')
  }
})

function streamChunkLengths (wireClassValue, contentLength) {
  const ciphertextLength = STREAM_WIRE_CLASS[wireClassValue]
  if (!ciphertextLength) fail('stream chunk wireClass is outside 1..3')
  if (!Number.isSafeInteger(contentLength) || contentLength < 0) fail('contentLength is outside u32')
  const plaintextLength = ciphertextLength - 16
  const paddingLength = plaintextLength - 7 - contentLength
  if (paddingLength < 0) fail('contentLength exceeds the selected stream wire class')
  return { plaintextLength, paddingLength }
}

function validateStreamChunkPlain (value) {
  if (!value || typeof value !== 'object') fail('BlindStreamChunkPlainV1 must be an object')
  if (value.flags !== 0 && value.flags !== 1) fail('stream chunk flags contain a reserved bit')
  const lengths = streamChunkLengths(value.wireClass, value.contentLength)
  fixedBytes(value.contentLength, 'stream content').preencode({ end: 0 }, value.content)
  fixedBytes(lengths.paddingLength, 'stream randomPadding').preencode({ end: 0 }, value.randomPadding)
  return lengths
}

export const blindStreamChunkPlainV1 = {
  preencode (state, value) {
    const { plaintextLength } = validateStreamChunkPlain(value)
    version1.preencode(state, value.version)
    wireClass.preencode(state, value.wireClass)
    u8.preencode(state, value.flags)
    u32be.preencode(state, value.contentLength)
    fixedBytes(value.contentLength, 'stream content').preencode(state, value.content)
    fixedBytes(plaintextLength - 7 - value.contentLength, 'stream randomPadding').preencode(state, value.randomPadding)
  },
  encode (state, value) {
    const { paddingLength } = validateStreamChunkPlain(value)
    version1.encode(state, value.version)
    wireClass.encode(state, value.wireClass)
    u8.encode(state, value.flags)
    u32be.encode(state, value.contentLength)
    fixedBytes(value.contentLength, 'stream content').encode(state, value.content)
    fixedBytes(paddingLength, 'stream randomPadding').encode(state, value.randomPadding)
  },
  decode (state) {
    const value = {
      version: version1.decode(state),
      wireClass: wireClass.decode(state),
      flags: u8.decode(state),
      contentLength: u32be.decode(state)
    }
    const { paddingLength } = streamChunkLengths(value.wireClass, value.contentLength)
    value.content = fixedBytes(value.contentLength, 'stream content').decode(state)
    value.randomPadding = fixedBytes(paddingLength, 'stream randomPadding').decode(state)
    validateStreamChunkPlain(value)
    return value
  }
}

const descriptorProtocolsV1 = arrayOf(protocolProfileV1, 1, 16, 'descriptor protocols')
const descriptorEndpointsV1 = arrayOf(transportEndpointV1, 1, 16, 'descriptor endpoints')
const descriptorAdmissionProfilesV1 = arrayOf(admissionProfileV1, 1, 8, 'descriptor admission profiles')

const blindServiceDescriptorBaseV1 = struct([
  ['version', version1],
  ['relayPublicKey', bytes32],
  ['storeId', bytes32],
  ['descriptorSequence', u64be],
  ['previousDescriptorHash', optional(bytes32, 'previousDescriptorHash')],
  ['identitySequence', u64be],
  ['previousRelayKey', optional(bytes32, 'previousRelayKey')],
  ['identityTransition', optional(relayIdentityTransitionV1, 'identityTransition')],
  ['build', buildProfileV1],
  ['protocols', descriptorProtocolsV1],
  ['endpoints', descriptorEndpointsV1],
  ['cellSizeClassBits', u8],
  ['leaseClassBits', u8],
  ['maxBatchCount', ranged(u16be, 1, PUBLIC_PROFILE_LIMITS.MAX_BATCH_COUNT, 'maxBatchCount')],
  ['maxResponseBytes', ranged(u32be, 1, PUBLIC_PROFILE_LIMITS.MAX_RESPONSE_BYTES, 'maxResponseBytes')],
  ['maxSponsoredCoreLength', u64be],
  ['enabledOperationBits', operationBits],
  ['admissionProfiles', descriptorAdmissionProfilesV1],
  ['durability', durabilityProfileV1],
  ['durabilityContinuityHash', bytes32],
  ['durabilityProfileHash', bytes32],
  ['storeLifecycleState', ranged(u8, STORE_LIFECYCLE_STATE.ACTIVE, STORE_LIFECYCLE_STATE.RETIRED,
    'storeLifecycleState')],
  ['drainStartedEpoch', optional(u32be, 'drainStartedEpoch')],
  ['capacityBand', ranged(u8, 0, 7, 'capacityBand')],
  ['issuedEpoch', u32be],
  ['expiresEpoch', u32be],
  ['descriptorNonce', bytes32],
  ['signature', bytes64]
], {
  name: 'BlindServiceDescriptorV1',
  validate (value) {
    for (const field of [
      'relayPublicKey',
      'storeId',
      'durabilityContinuityHash',
      'durabilityProfileHash',
      'descriptorNonce'
    ]) assertNonzeroBytes(value[field], field)

    const descriptorSequence = bigint(value.descriptorSequence, 'descriptorSequence')
    if ((descriptorSequence === 0n) !== (value.previousDescriptorHash == null)) {
      fail('previousDescriptorHash must be absent exactly for descriptor sequence zero')
    }
    const rotating = value.identityTransition != null
    if (rotating !== (value.previousRelayKey != null)) {
      fail('previousRelayKey and identityTransition must be present together')
    }
    if (rotating) {
      const transition = value.identityTransition
      if (!b4a.equals(value.previousRelayKey, transition.oldRelayKey) ||
          !b4a.equals(value.relayPublicKey, transition.newRelayKey) ||
          bigint(value.identitySequence, 'identitySequence') !==
            bigint(transition.newIdentitySequence, 'newIdentitySequence')) {
        fail('descriptor identity transition does not bind its current and previous identity')
      }
    }

    assertStrictlySorted(value.protocols, (a, b) => compareScalar(a.protocolId, b.protocolId),
      'descriptor protocols')
    assertStrictlySorted(value.endpoints, (a, b) => compareScalar(a.endpointId, b.endpointId),
      'descriptor endpoints')
    assertStrictlySorted(value.admissionProfiles, (a, b) => compareScalar(a.profileId, b.profileId),
      'descriptor admission profiles')

    if (value.cellSizeClassBits === 0 ||
        (value.cellSizeClassBits & ~PUBLIC_PROFILE_LIMITS.CELL_SIZE_CLASS_BITS_MASK) !== 0) {
      fail('cellSizeClassBits must contain known nonzero bits')
    }
    if (value.leaseClassBits === 0 ||
        (value.leaseClassBits & ~PUBLIC_PROFILE_LIMITS.LEASE_CLASS_BITS_MASK) !== 0) {
      fail('leaseClassBits must contain known nonzero bits')
    }
    if ((value.enabledOperationBits & ~PUBLIC_PROFILE_LIMITS.ENABLED_OPERATION_BITS_MASK) !== 0) {
      fail('enabledOperationBits contains a reserved bit')
    }

    const draining = value.storeLifecycleState === STORE_LIFECYCLE_STATE.DRAINING
    const retired = value.storeLifecycleState === STORE_LIFECYCLE_STATE.RETIRED
    if ((value.storeLifecycleState === STORE_LIFECYCLE_STATE.ACTIVE) !== (value.drainStartedEpoch == null)) {
      fail('drainStartedEpoch presence does not match storeLifecycleState')
    }
    if (value.drainStartedEpoch != null && value.drainStartedEpoch > value.issuedEpoch) {
      fail('drainStartedEpoch cannot be after descriptor issuance')
    }
    if (draining && value.enabledOperationBits !== 0x000129d7) {
      fail('DRAINING descriptor has the wrong enabled operation bitmap')
    }
    if (retired && value.enabledOperationBits !== 0) fail('RETIRED descriptor must disable all operations')
    validateEpochWindow(value)
  }
})

export const blindServiceDescriptorV1 = cappedEncoding(blindServiceDescriptorBaseV1,
  PUBLIC_PROFILE_LIMITS.MAX_DESCRIPTOR_BYTES, 'BlindServiceDescriptorV1')

export const schemaCatalogEntryV1 = struct([
  ['category', ranged(u8, 1, 5, 'schema category')],
  ['categoryLocalSchemaId', ranged(u16be, 1, 0xffff, 'categoryLocalSchemaId')],
  ['schemaName', canonicalAsciiBytes(1, 96, 'schemaName')],
  ['canonicalSchemaBytes', boundedBytes(1, 0xffff, 'canonicalSchemaBytes')]
], {
  name: 'SchemaCatalogEntryV1',
  validate (value) {
    const hashes = SCHEMA_CATALOG_NAME_HASHES_BY_CATEGORY[value.category]
    const expectedHash = hashes && hashes[value.categoryLocalSchemaId - 1]
    if (!expectedHash ||
        !b4a.equals(blake2b256(value.schemaName), b4a.from(expectedHash, 'hex'))) {
      fail('schema catalog entry does not match the frozen category registry')
    }
  }
})

export const PUBLIC_FAMILY_SCHEMA_CODECS = Object.freeze({
  AdmissionParametersV1: admissionParametersV1,
  AdmissionProfileV1: admissionProfileV1,
  BlindCoreAckV1: blindCoreAckV1,
  BlindDhtPointerV1: blindDhtPointerV1,
  BlindExternalJournalTopologyV1: blindExternalJournalTopologyV1,
  BlindForwardCloseV1: blindForwardCloseV1,
  BlindForwardDataV1: blindForwardDataV1,
  BlindForwardOpenResultV1: blindForwardOpenResultV1,
  BlindForwardOpenV1: blindForwardOpenV1,
  BlindForwardWindowV1: blindForwardWindowV1,
  BlindHealthChallengeV1: blindHealthChallengeV1,
  BlindHealthResultV1: blindHealthResultV1,
  BlindOhttpKeyConfigV1: blindOhttpKeyConfigV1,
  BlindServiceDescriptorV1: blindServiceDescriptorV1,
  BlindStreamChunkPlainV1: blindStreamChunkPlainV1,
  CoreMirrorRequestV1: coreMirrorRequestV1,
  CoreServeChallengeV1: coreServeChallengeV1,
  CoreServeResultV1: coreServeResultV1,
  RelayIdentityTransitionV1: relayIdentityTransitionV1,
  SchemaCatalogEntryV1: schemaCatalogEntryV1
})