OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / packages / blind-protocol / test / frozen-control-codecs.test.js

376 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import {
  ABI_STATUS,
  ABI_RELEASE_BLOCKERS,
  DISPATCH_LIMITS,
  ERROR_CODE,
  FAMILY,
  FRAME_KIND,
  HASH_DOMAIN,
  IMPLEMENTED_SCHEMAS,
  OHTTP_TRANSPORT_ERROR_CODE,
  OHTTP_TRANSPORT_ERROR_ROWS,
  OPERATION_CAP_ROWS,
  OPERATION,
  REQUIRED_SCHEMA_NAMES,
  STREAM_WIRE_CLASS,
  blindForwardHopAcceptV1,
  blindForwardHopOpenV1,
  blindForwardRouteHopV1,
  blindForwardRouteScopeV1,
  blindOhttpTransportErrorV1,
  blindTransportRouteV1,
  blake2b256,
  coreOpenReplicationRequestCommitment,
  coreOpenReplicationResultV1,
  coreOpenReplicationV1,
  decodeCanonical,
  decodeDispatchFrame,
  draftAbiRegistryValue,
  encodeCanonical,
  encodeDispatchFrame,
  forwardRouteScopeGenesisHash,
  forwardRouteScopeHopHash
} from '../index.js'

const KiB = 1024
const MiB = 1024 * KiB
const bytes = (length, value) => b4a.alloc(length, value)

function relayBinding (seed, overrides = {}) {
  return {
    version: 1,
    relayPublicKey: bytes(32, seed),
    storeId: bytes(32, seed + 1),
    descriptorSequence: 1n,
    descriptorHash: bytes(32, seed + 2),
    durabilityProfileId: 1,
    durabilityContinuityHash: bytes(32, seed + 3),
    durabilityProfileHash: bytes(32, seed + 4),
    restoreEvidenceHeadSequence: 0n,
    restoreEvidenceHeadHash: bytes(32, 0),
    externalCommitWitness: null,
    ...overrides
  }
}

function admission () {
  return {
    profileId: 7,
    schemeId: 9,
    parameterHash: bytes(32, 0xa1),
    token: bytes(1, 0xa2)
  }
}

function streamingRoute () {
  return {
    version: 1,
    routeKind: 2,
    routeId: bytes(16, 0x10),
    previousRelayKey: bytes(32, 0x11),
    previousEndpointId: 1,
    nextRelayKey: bytes(32, 0x12),
    nextDescriptorSequence: 2n,
    nextDescriptorHash: bytes(32, 0x13),
    nextEndpointId: 2,
    envelopeClassBits: 0,
    wireClassBits: 0x0e,
    maxCanonicalDispatchBytes: DISPATCH_LIMITS.MAX_WIRE_BYTES,
    maxEncapsulatedRequestBytes: 0,
    maxOpenBytes: 128 * KiB,
    maxCircuitBytes: 256n * BigInt(MiB),
    maxConcurrentStreams: 16,
    maxRelayCount: 4,
    hopAdmissionProfileId: 7,
    issuedEpoch: 20,
    expiresEpoch: 24,
    routeNonce: bytes(32, 0x14),
    previousSignature: bytes(64, 0x15)
  }
}

function routeScope () {
  const root = {
    version: 1,
    rootRouteId: bytes(16, 0x10),
    rootCircuitNonce: bytes(32, 0x21),
    rootRequestCommitment: bytes(32, 0x22),
    maxRelayCount: 4,
    expiresEpoch: 24
  }
  const genesis = forwardRouteScopeGenesisHash(root)
  const hop = {
    hopIndex: 0,
    relayPublicKey: bytes(32, 0x11),
    descriptorSequence: 3n,
    descriptorHash: bytes(32, 0x20),
    previousScopeHash: bytes(32, 0)
  }
  return {
    ...root,
    hops: [{
      ...hop,
      scopeHash: forwardRouteScopeHopHash({ ...hop, previousScopeHash: genesis }),
      relaySignature: bytes(64, 0x25)
    }]
  }
}

function forwardHopOpen () {
  return {
    version: 1,
    route: streamingRoute(),
    routeScope: routeScope(),
    previousDescriptorSequence: 3n,
    previousDescriptorHash: bytes(32, 0x20),
    circuitNonce: bytes(32, 0x21),
    requestedWireClass: 3,
    circuitClass: 3,
    grantedInitialWindow: MiB,
    maxDataBytes: 65535,
    maxCircuitBytes: 256n * BigInt(MiB),
    idleMillis: 120_000,
    lifetimeMillis: 60 * 60_000,
    clientRequestCommitment: bytes(32, 0x22),
    handshakeFlight1: bytes(32, 0x23),
    forwarderSignature: bytes(64, 0x24)
  }
}

function forwardHopAccept () {
  return {
    version: 1,
    previousRelayKey: bytes(32, 0x30),
    previousDescriptorSequence: 3n,
    previousDescriptorHash: bytes(32, 0x31),
    nextRelayKey: bytes(32, 0x32),
    nextDescriptorSequence: 4n,
    nextDescriptorHash: bytes(32, 0x33),
    nextRelayBinding: relayBinding(0x32, {
      relayPublicKey: bytes(32, 0x32),
      descriptorSequence: 4n,
      descriptorHash: bytes(32, 0x33)
    }),
    routeId: bytes(16, 0x34),
    circuitNonce: bytes(32, 0x35),
    nextStreamId: 5n,
    grantedWireClass: 3,
    circuitClass: 3,
    grantedInitialWindow: MiB,
    maxDataBytes: 65535,
    maxCircuitBytes: 256n * BigInt(MiB),
    idleMillis: 120_000,
    lifetimeMillis: 60 * 60_000,
    openedAtEpoch: 21,
    hopOpenCommitment: bytes(32, 0x36),
    acceptedRouteScopeHash: bytes(32, 0x39),
    acceptedRelayCount: 1,
    handshakeFlight2: bytes(96, 0x37),
    nextSignature: bytes(64, 0x38)
  }
}

test('blind registry: frozen codes, wire class and schema gate stay synchronized', t => {
  t.is(ERROR_CODE.TRANSPORT_UNSUPPORTED, 20)
  t.is(STREAM_WIRE_CLASS[3], 65535)
  t.is(DISPATCH_LIMITS.MAX_FORWARD_DATA_BYTES, 65535)
  t.is(REQUIRED_SCHEMA_NAMES.length, 152)
  t.is(IMPLEMENTED_SCHEMAS.length, 145)
  t.is(ABI_STATUS.missingSchemaNames.length, 0)
  t.alike(ABI_RELEASE_BLOCKERS, [])
  t.is(ABI_STATUS.releaseReady, true)
  const draftRegistry = draftAbiRegistryValue()
  t.is(draftRegistry.dispatchLimits.find(limit => limit.name === 'MAX_FORWARD_DATA_BYTES').value, 65535)
  t.is(draftRegistry.forwardCircuitClasses.find(entry => entry.id === 2).maxCircuitBytes, 64 * MiB)
  t.is(draftRegistry.coreSessionClasses.find(entry => entry.id === 2).maxSessionBytes, 64 * MiB)
  for (const name of [
    'AdmissionProfileV1',
    'BlindCoreReadCapV1',
    'BlindForwardHopAcceptV1',
    'BlindForwardHopOpenV1',
    'BlindForwardRouteHopV1',
    'BlindForwardRouteScopeV1',
    'BlindLocalCheckpointV1',
    'BlindOhttpTransportErrorV1',
    'BlindStoreManifestV1',
    'BlindWalHeaderV2',
    'BuildManifestV1',
    'CellBlobV1',
    'CellRecordV1',
    'ChargedUnaryRetryV1',
    'CoreOpenReplicationResultV1',
    'CoreOpenReplicationV1',
    'DurabilityProfileV1',
    'OpaqueChainCheckpointV1',
    'OpaqueChainFrameV1',
    'ReadCellCapV1',
    'WriteCellCapV1'
  ]) t.ok(REQUIRED_SCHEMA_NAMES.includes(name), `${name} is release-gated`)
})

test('blind OHTTP transport error: protected pre-dispatch code is exactly two bytes', t => {
  t.is(OHTTP_TRANSPORT_ERROR_CODE.MALFORMED_INNER, 1)
  t.is(OHTTP_TRANSPORT_ERROR_CODE.TARGET_UNAVAILABLE, 2)
  t.is(OHTTP_TRANSPORT_ERROR_CODE.TARGET_TIMEOUT, 3)
  t.alike(OHTTP_TRANSPORT_ERROR_ROWS.map(row => [row.code, row.protectedStatus, row.deliveryBoundary, row.retryAction]), [
    [1, 400, 1, 0],
    [2, 503, 2, 1],
    [3, 504, 3, 2]
  ])
  t.is(OPERATION_CAP_ROWS.length, 22)
  for (const code of [1, 2, 3]) {
    const encoded = encodeCanonical(blindOhttpTransportErrorV1, { version: 1, code })
    t.alike(encoded, b4a.from([1, code]))
    t.is(decodeCanonical(blindOhttpTransportErrorV1, encoded).code, code)
  }
  t.exception(() => encodeCanonical(blindOhttpTransportErrorV1, { version: 1, code: 0 }), /outside 1..3/)
  t.exception(() => encodeCanonical(blindOhttpTransportErrorV1, { version: 1, code: 4 }), /outside 1..3/)
})

test('blind forwarding: route and adjacent-hop records are byte-exact', t => {
  const route = encodeCanonical(blindTransportRouteV1, streamingRoute())
  t.is(route.byteLength, 256)
  t.is(decodeCanonical(blindTransportRouteV1, route).nextDescriptorSequence, 2n)

  const open = encodeCanonical(blindForwardHopOpenV1, forwardHopOpen())
  t.is(open.byteLength, 771)
  const decodedOpen = decodeCanonical(blindForwardHopOpenV1, open)
  t.is(decodedOpen.maxDataBytes, 65535)
  t.alike(decodedOpen.handshakeFlight1, bytes(32, 0x23))

  const accept = encodeCanonical(blindForwardHopAcceptV1, forwardHopAccept())
  t.is(accept.byteLength, 667)
  const decodedAccept = decodeCanonical(blindForwardHopAcceptV1, accept)
  t.is(decodedAccept.nextStreamId, 5n)
  t.alike(decodedAccept.handshakeFlight2, bytes(96, 0x37))

  const scope = encodeCanonical(blindForwardRouteScopeV1, routeScope())
  t.is(decodeCanonical(blindForwardRouteHopV1,
    encodeCanonical(blindForwardRouteHopV1, routeScope().hops[0])).hopIndex, 0)
  t.alike(decodeCanonical(blindForwardRouteScopeV1, scope).hops[0].relayPublicKey, bytes(32, 0x11))
})

test('blind forwarding: route, class and stream invariants fail closed', t => {
  t.exception(() => encodeCanonical(blindTransportRouteV1, {
    ...streamingRoute(),
    expiresEpoch: 25
  }), /route epoch window/)
  t.exception(() => encodeCanonical(blindTransportRouteV1, {
    ...streamingRoute(),
    wireClassBits: 0x80
  }), /reserved bit/)
  t.exception(() => encodeCanonical(blindForwardHopOpenV1, {
    ...forwardHopOpen(),
    maxDataBytes: 64 * KiB
  }), /selected wire class/)
  t.exception(() => encodeCanonical(blindForwardHopOpenV1, {
    ...forwardHopOpen(),
    grantedInitialWindow: 256 * KiB
  }), /frozen class tuple/)
  t.exception(() => encodeCanonical(blindForwardHopAcceptV1, {
    ...forwardHopAccept(),
    nextStreamId: 0n
  }), /must be nonzero/)
})

test('blind core: open and signed result freeze the upstream child limits', t => {
  const open = {
    version: 1,
    wireProfileHash: bytes(32, 0x40),
    sessionClass: 2,
    controlChannelId: 0x0102030405060708n,
    parentChannelBinding: bytes(32, 0x47),
    clientNonce: bytes(32, 0x41),
    admission: admission()
  }
  const encodedOpen = encodeCanonical(coreOpenReplicationV1, open)
  t.is(encodedOpen.byteLength, 144)
  t.is(decodeCanonical(coreOpenReplicationV1, encodedOpen).sessionClass, 2)
  t.is(decodeCanonical(coreOpenReplicationV1, encodedOpen).controlChannelId, 0x0102030405060708n)
  t.alike(decodeCanonical(coreOpenReplicationV1, encodedOpen).parentChannelBinding, bytes(32, 0x47))
  const commitment = coreOpenReplicationRequestCommitment({
    relayPublicKey: bytes(32, 0x42),
    wireProfileHash: open.wireProfileHash,
    sessionClass: open.sessionClass,
    controlChannelId: open.controlChannelId,
    parentChannelBinding: open.parentChannelBinding,
    clientNonce: open.clientNonce
  })
  t.alike(commitment, blake2b256(b4a.concat([
    b4a.from(HASH_DOMAIN.REQUEST, 'ascii'),
    b4a.from('core-open-replication', 'ascii'),
    bytes(32, 0x42),
    open.wireProfileHash,
    b4a.from([2]),
    b4a.from('0102030405060708', 'hex'),
    open.parentChannelBinding,
    open.clientNonce
  ])))

  const result = {
    version: 1,
    relayBinding: relayBinding(0x42),
    wireProfileHash: bytes(32, 0x43),
    sessionClass: 2,
    controlChannelId: 0x0102030405060708n,
    parentChannelBinding: bytes(32, 0x47),
    streamId: 9n,
    maxSessionBytes: 64n * BigInt(MiB),
    idleMillis: 60_000,
    lifetimeMillis: 30 * 60_000,
    openedAtEpoch: 50,
    requestNonce: bytes(32, 0x44),
    requestCommitment: bytes(32, 0x45),
    signature: bytes(64, 0x46)
  }
  const encodedResult = encodeCanonical(coreOpenReplicationResultV1, result)
  t.is(encodedResult.byteLength, 441)
  t.is(decodeCanonical(coreOpenReplicationResultV1, encodedResult).maxSessionBytes, 64n * BigInt(MiB))
  t.exception(() => encodeCanonical(coreOpenReplicationResultV1, {
    ...result,
    maxSessionBytes: 16n * BigInt(MiB)
  }), /frozen class tuple/)
  t.exception(() => encodeCanonical(coreOpenReplicationResultV1, {
    ...result,
    streamId: 0n
  }), /must be nonzero/)
  t.exception(() => encodeCanonical(coreOpenReplicationV1, {
    ...open,
    parentChannelBinding: bytes(32, 0)
  }), /must be nonzero/)
  t.exception(() => encodeCanonical(coreOpenReplicationV1, {
    ...open,
    controlChannelId: 0n
  }), /must be nonzero/)
  t.exception(() => coreOpenReplicationRequestCommitment({
    relayPublicKey: result.relayBinding.relayPublicKey,
    ...open,
    controlChannelId: 0n
  }), /must be nonzero/)
})

test('blind dispatch: CORE open returns a child but never uses kind-4 frames', t => {
  const requestId = bytes(16, 0x51)
  const response = encodeDispatchFrame({
    frameKind: FRAME_KIND.RESPONSE,
    familyId: FAMILY.CORE,
    operationId: OPERATION.CORE.OPEN_REPLICATION,
    requestId,
    streamId: 7n,
    body: b4a.alloc(0)
  })
  t.is(decodeDispatchFrame(response).streamId, 7n)

  t.exception(() => encodeDispatchFrame({
    frameKind: FRAME_KIND.STREAM,
    familyId: FAMILY.CORE,
    operationId: OPERATION.CORE.OPEN_REPLICATION,
    requestId: b4a.alloc(16),
    streamId: 7n,
    sequence: 1n,
    body: bytes(1, 0x52)
  }), /frame kind is not allowed for the operation/)
})