OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / blind-protocol-dispatch.test.js

153 lines · javascript ref:
import test from 'brittle'
import b4a from 'b4a'
import {
  DISPATCH_LIMITS,
  DispatchSequenceGuard,
  FAMILY,
  FRAME_KIND,
  MAX_U64,
  OPERATION,
  decodeDispatchFrame,
  encodeDispatchFrame,
  readDispatchLengthPrefix
} from '@hiverelay/blind-protocol'

const requestId = (byte = 1) => b4a.alloc(16, byte)
const zeroRequestId = () => b4a.alloc(16)

test('blind dispatch: unary request round-trips byte-exact fields', (t) => {
  const body = b4a.from('opaque')
  const encoded = encodeDispatchFrame({
    frameKind: FRAME_KIND.REQUEST,
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.GET,
    requestId: requestId(0x11),
    body
  })
  t.is(b4a.readUInt32BE(encoded, 0), DISPATCH_LIMITS.HEADER_BYTES + body.byteLength)
  const decoded = decodeDispatchFrame(encoded)
  t.is(decoded.version, 1)
  t.is(decoded.frameKind, FRAME_KIND.REQUEST)
  t.is(decoded.familyId, FAMILY.CELL)
  t.is(decoded.operationId, OPERATION.CELL.GET)
  t.ok(b4a.equals(decoded.requestId, requestId(0x11)))
  t.is(decoded.streamId, 0n)
  t.is(decoded.sequence, 0n)
  t.ok(b4a.equals(decoded.body, body))
})

test('blind dispatch: streaming open response and stream data round-trip u64', (t) => {
  const open = encodeDispatchFrame({
    frameKind: FRAME_KIND.RESPONSE,
    familyId: FAMILY.FORWARD,
    operationId: OPERATION.FORWARD.OPEN,
    requestId: requestId(0x22),
    streamId: MAX_U64,
    body: b4a.alloc(0)
  })
  t.is(decodeDispatchFrame(open).streamId, MAX_U64)

  const data = encodeDispatchFrame({
    frameKind: FRAME_KIND.STREAM,
    familyId: FAMILY.FORWARD,
    operationId: OPERATION.FORWARD.DATA,
    requestId: zeroRequestId(),
    streamId: 9n,
    sequence: 7n,
    body: b4a.from('ciphertext')
  })
  const decoded = decodeDispatchFrame(data)
  t.is(decoded.streamId, 9n)
  t.is(decoded.sequence, 7n)
})

test('blind dispatch: rejects unknown IDs, reserved flags and invalid correlations', (t) => {
  const base = {
    frameKind: FRAME_KIND.REQUEST,
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.GET,
    requestId: requestId(),
    body: b4a.alloc(0)
  }
  t.exception(() => encodeDispatchFrame({ ...base, operationId: 255 }), /unknown family\/operation/)
  t.exception(() => encodeDispatchFrame({ ...base, flags: 1 }), /flags are reserved/)
  t.exception(() => encodeDispatchFrame({ ...base, requestId: zeroRequestId() }), /correlation fields/)
  t.exception(() => encodeDispatchFrame({ ...base, streamId: 1n }), /correlation fields/)
  t.exception(() => encodeDispatchFrame({ ...base, streamId: -1n }), /outside u64/)
  t.exception(() => encodeDispatchFrame({ ...base, streamId: MAX_U64 + 1n }), /outside u64/)
  t.exception(() => encodeDispatchFrame({
    ...base,
    frameKind: FRAME_KIND.STREAM,
    requestId: zeroRequestId(),
    streamId: 1n
  }), /frame kind is not allowed/)
})

test('blind dispatch: declared caps reject before body materialization', (t) => {
  const prefix = b4a.alloc(4)
  b4a.writeUInt32BE(prefix, DISPATCH_LIMITS.MAX_FRAME_AFTER_PREFIX_BYTES + 1, 0)
  t.exception(() => readDispatchLengthPrefix(prefix), /absolute cap/)
  t.exception(() => encodeDispatchFrame({
    frameKind: FRAME_KIND.REQUEST,
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.PUT,
    requestId: requestId(),
    body: b4a.alloc(DISPATCH_LIMITS.MAX_BODY_BYTES + 1)
  }), /body exceeds/)
  t.exception(() => encodeDispatchFrame({
    frameKind: FRAME_KIND.REQUEST,
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.GET,
    requestId: requestId(),
    body: b4a.alloc(16385)
  }), /operation cap/)

  const header = encodeDispatchFrame({
    frameKind: FRAME_KIND.REQUEST,
    familyId: FAMILY.CELL,
    operationId: OPERATION.CELL.GET,
    requestId: requestId(),
    body: b4a.alloc(0)
  })
  const oversized = b4a.concat([header, b4a.alloc(16385)])
  b4a.writeUInt32BE(oversized, DISPATCH_LIMITS.HEADER_BYTES + 16385, 0)
  b4a.writeUInt32BE(oversized, 16385, 41)
  t.exception(() => decodeDispatchFrame(oversized, { copyBody: true }), /operation cap/)
})

test('blind dispatch: malformed, truncated and trailing frames fail closed', (t) => {
  const encoded = encodeDispatchFrame({
    frameKind: FRAME_KIND.REQUEST,
    familyId: FAMILY.DESCRIBE,
    operationId: OPERATION.DESCRIBE.GET,
    requestId: requestId(),
    body: b4a.from('x')
  })
  t.exception(() => decodeDispatchFrame(encoded.subarray(0, encoded.length - 1)), /length mismatch/)
  t.exception(() => decodeDispatchFrame(b4a.concat([encoded, b4a.from([0])])), /length mismatch/)

  const badBodyLength = b4a.from(encoded)
  b4a.writeUInt32BE(badBodyLength, 99, 41)
  t.exception(() => decodeDispatchFrame(badBodyLength), /body length mismatch/)

  const badVersion = b4a.from(encoded)
  badVersion[4] = 2
  t.exception(() => decodeDispatchFrame(badVersion), /version must be 1/)

  const badOperation = b4a.from(encoded)
  badOperation[7] = 255
  t.exception(() => decodeDispatchFrame(badOperation), /unknown family\/operation/)
})

test('blind dispatch: sequence guard bounds streams and rejects rollback', (t) => {
  const guard = new DispatchSequenceGuard({ maxStreams: 1 })
  const frame = { frameKind: FRAME_KIND.STREAM, streamId: 3n, sequence: 0n }
  guard.accept(frame)
  guard.accept({ ...frame, sequence: 1n })
  t.exception(() => guard.accept({ ...frame, sequence: 1n }), /non-monotonic/)
  t.exception(() => guard.accept({ ...frame, streamId: 4n }), /guard is full/)
  guard.close(3n)
  guard.accept({ ...frame, streamId: 4n })
  guard.clear()
})